From 316415c33b2c2c2c29fe7d65ce0718c7f3457ee7 Mon Sep 17 00:00:00 2001 From: corolin Date: Sat, 22 Mar 2025 16:56:44 +0800 Subject: [PATCH 01/46] fix issue #531 --- .github/workflows/docker-image.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index e88dbf63b..c06d967ca 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -22,18 +22,18 @@ jobs: - name: Login to Docker Hub uses: docker/login-action@v3 with: - username: ${{ vars.DOCKERHUB_USERNAME }} + username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Determine Image Tags id: tags run: | if [[ "${{ github.ref }}" == refs/tags/* ]]; then - echo "tags=${{ vars.DOCKERHUB_USERNAME }}/maimbot:${{ github.ref_name }},${{ vars.DOCKERHUB_USERNAME }}/maimbot:latest" >> $GITHUB_OUTPUT + echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:${{ github.ref_name }},${{ secrets.DOCKERHUB_USERNAME }}/maimbot:latest" >> $GITHUB_OUTPUT elif [ "${{ github.ref }}" == "refs/heads/main" ]; then - echo "tags=${{ vars.DOCKERHUB_USERNAME }}/maimbot:main,${{ vars.DOCKERHUB_USERNAME }}/maimbot:latest" >> $GITHUB_OUTPUT + echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:main,${{ secrets.DOCKERHUB_USERNAME }}/maimbot:latest" >> $GITHUB_OUTPUT elif [ "${{ github.ref }}" == "refs/heads/main-fix" ]; then - echo "tags=${{ vars.DOCKERHUB_USERNAME }}/maimbot:main-fix" >> $GITHUB_OUTPUT + echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:main-fix" >> $GITHUB_OUTPUT fi - name: Build and Push Docker Image @@ -44,5 +44,5 @@ jobs: platforms: linux/amd64,linux/arm64 tags: ${{ steps.tags.outputs.tags }} push: true - cache-from: type=registry,ref=${{ vars.DOCKERHUB_USERNAME }}/maimbot:buildcache - cache-to: type=registry,ref=${{ vars.DOCKERHUB_USERNAME }}/maimbot:buildcache,mode=max + cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:buildcache + cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:buildcache,mode=max From 8bad58afe240321d40f6ac88c4402c8a5bc87b85 Mon Sep 17 00:00:00 2001 From: zzzzzyc <104435384+zzzzzyc@users.noreply.github.com> Date: Sat, 22 Mar 2025 22:24:39 +0800 Subject: [PATCH 02/46] Add files via upload --- 配置文件错误排查.py | 617 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 617 insertions(+) create mode 100644 配置文件错误排查.py diff --git a/配置文件错误排查.py b/配置文件错误排查.py new file mode 100644 index 000000000..114171135 --- /dev/null +++ b/配置文件错误排查.py @@ -0,0 +1,617 @@ +import tomli +import sys +import re +from pathlib import Path +from typing import Dict, Any, List, Set, Tuple + +def load_toml_file(file_path: str) -> Dict[str, Any]: + """加载TOML文件""" + try: + with open(file_path, "rb") as f: + return tomli.load(f) + except Exception as e: + print(f"错误: 无法加载配置文件 {file_path}: {str(e)} 请检查文件是否存在或者他妈的有没有东西没写值") + sys.exit(1) + +def load_env_file(file_path: str) -> Dict[str, str]: + """加载.env文件中的环境变量""" + env_vars = {} + try: + with open(file_path, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + if not line or line.startswith('#'): + continue + if '=' in line: + key, value = line.split('=', 1) + key = key.strip() + value = value.strip() + + # 处理注释 + if '#' in value: + value = value.split('#', 1)[0].strip() + + # 处理引号 + if (value.startswith('"') and value.endswith('"')) or \ + (value.startswith("'") and value.endswith("'")): + value = value[1:-1] + + env_vars[key] = value + return env_vars + except Exception as e: + print(f"警告: 无法加载.env文件 {file_path}: {str(e)}") + return {} + +def check_required_sections(config: Dict[str, Any]) -> List[str]: + """检查必要的配置段是否存在""" + required_sections = [ + "inner", "bot", "personality", "message", "emoji", + "cq_code", "response", "willing", "memory", "mood", + "groups", "model" + ] + missing_sections = [] + + for section in required_sections: + if section not in config: + missing_sections.append(section) + + return missing_sections + +def check_probability_sum(config: Dict[str, Any]) -> List[Tuple[str, float]]: + """检查概率总和是否为1""" + errors = [] + + # 检查人格概率 + if "personality" in config: + personality = config["personality"] + prob_sum = sum([ + personality.get("personality_1_probability", 0), + personality.get("personality_2_probability", 0), + personality.get("personality_3_probability", 0) + ]) + if abs(prob_sum - 1.0) > 0.001: # 允许有小数点精度误差 + errors.append(("人格概率总和", prob_sum)) + + # 检查响应模型概率 + if "response" in config: + response = config["response"] + model_prob_sum = sum([ + response.get("model_r1_probability", 0), + response.get("model_v3_probability", 0), + response.get("model_r1_distill_probability", 0) + ]) + if abs(model_prob_sum - 1.0) > 0.001: + errors.append(("响应模型概率总和", model_prob_sum)) + + return errors + +def check_probability_range(config: Dict[str, Any]) -> List[Tuple[str, float]]: + """检查概率值是否在0-1范围内""" + errors = [] + + # 收集所有概率值 + prob_fields = [] + + # 人格概率 + if "personality" in config: + personality = config["personality"] + prob_fields.extend([ + ("personality.personality_1_probability", personality.get("personality_1_probability")), + ("personality.personality_2_probability", personality.get("personality_2_probability")), + ("personality.personality_3_probability", personality.get("personality_3_probability")) + ]) + + # 消息概率 + if "message" in config: + message = config["message"] + prob_fields.append(("message.emoji_chance", message.get("emoji_chance"))) + + # 响应模型概率 + if "response" in config: + response = config["response"] + prob_fields.extend([ + ("response.model_r1_probability", response.get("model_r1_probability")), + ("response.model_v3_probability", response.get("model_v3_probability")), + ("response.model_r1_distill_probability", response.get("model_r1_distill_probability")) + ]) + + # 情绪衰减率 + if "mood" in config: + mood = config["mood"] + prob_fields.append(("mood.mood_decay_rate", mood.get("mood_decay_rate"))) + + # 中文错别字概率 + if "chinese_typo" in config and config["chinese_typo"].get("enable", False): + typo = config["chinese_typo"] + prob_fields.extend([ + ("chinese_typo.error_rate", typo.get("error_rate")), + ("chinese_typo.tone_error_rate", typo.get("tone_error_rate")), + ("chinese_typo.word_replace_rate", typo.get("word_replace_rate")) + ]) + + # 检查所有概率值是否在0-1范围内 + for field_name, value in prob_fields: + if value is not None and (value < 0 or value > 1): + errors.append((field_name, value)) + + return errors + +def check_model_configurations(config: Dict[str, Any], env_vars: Dict[str, str]) -> List[str]: + """检查模型配置是否完整,并验证provider是否正确""" + errors = [] + + if "model" not in config: + return ["缺少[model]部分"] + + required_models = [ + "llm_reasoning", "llm_reasoning_minor", "llm_normal", + "llm_normal_minor", "llm_emotion_judge", "llm_topic_judge", + "llm_summary_by_topic", "vlm", "embedding" + ] + + # 从环境变量中提取有效的API提供商 + valid_providers = set() + for key in env_vars: + if key.endswith('_BASE_URL'): + provider_name = key.replace('_BASE_URL', '') + valid_providers.add(provider_name) + + # 将provider名称标准化以便比较 + provider_mapping = { + "SILICONFLOW": ["SILICONFLOW", "SILICON_FLOW", "SILICON-FLOW"], + "CHAT_ANY_WHERE": ["CHAT_ANY_WHERE", "CHAT-ANY-WHERE", "CHATANYWHERE"], + "DEEP_SEEK": ["DEEP_SEEK", "DEEP-SEEK", "DEEPSEEK"] + } + + # 创建反向映射表,用于检查错误拼写 + reverse_mapping = {} + for standard, variants in provider_mapping.items(): + for variant in variants: + reverse_mapping[variant.upper()] = standard + + for model_name in required_models: + # 检查model下是否有对应子部分 + if model_name not in config["model"]: + errors.append(f"缺少[model.{model_name}]配置") + else: + model_config = config["model"][model_name] + if "name" not in model_config: + errors.append(f"[model.{model_name}]缺少name属性") + + if "provider" not in model_config: + errors.append(f"[model.{model_name}]缺少provider属性") + else: + provider = model_config["provider"].upper() + + # 检查拼写错误 + for known_provider, correct_provider in reverse_mapping.items(): + # 使用模糊匹配检测拼写错误 + if provider != known_provider and _similar_strings(provider, known_provider) and provider not in reverse_mapping: + errors.append(f"[model.{model_name}]的provider '{model_config['provider']}' 可能拼写错误,应为 '{known_provider}'") + break + + return errors + +def _similar_strings(s1: str, s2: str) -> bool: + """简单检查两个字符串是否相似(用于检测拼写错误)""" + # 如果两个字符串长度相差过大,则认为不相似 + if abs(len(s1) - len(s2)) > 2: + return False + + # 计算相同字符的数量 + common_chars = sum(1 for c1, c2 in zip(s1, s2) if c1 == c2) + # 如果相同字符比例超过80%,则认为相似 + return common_chars / max(len(s1), len(s2)) > 0.8 + +def check_api_providers(config: Dict[str, Any], env_vars: Dict[str, str]) -> List[str]: + """检查配置文件中的API提供商是否与环境变量中的一致""" + errors = [] + + if "model" not in config: + return ["缺少[model]部分"] + + # 从环境变量中提取有效的API提供商 + valid_providers = {} + for key in env_vars: + if key.endswith('_BASE_URL'): + provider_name = key.replace('_BASE_URL', '') + base_url = env_vars[key] + valid_providers[provider_name] = { + "base_url": base_url, + "key": env_vars.get(f"{provider_name}_KEY", "") + } + + # 检查配置文件中使用的所有提供商 + used_providers = set() + for model_category, model_config in config["model"].items(): + if "provider" in model_config: + provider = model_config["provider"] + used_providers.add(provider) + + # 检查此提供商是否在环境变量中定义 + normalized_provider = provider.replace(" ", "_").upper() + found = False + for env_provider in valid_providers: + if normalized_provider == env_provider: + found = True + break + # 尝试更宽松的匹配(例如SILICONFLOW可能匹配SILICON_FLOW) + elif normalized_provider.replace("_", "") == env_provider.replace("_", ""): + found = True + errors.append(f"提供商 '{provider}' 在环境变量中的名称是 '{env_provider}', 建议统一命名") + break + + if not found: + errors.append(f"提供商 '{provider}' 在环境变量中未定义") + + # 特别检查常见的拼写错误 + for provider in used_providers: + if provider.upper() == "SILICONFOLW": + errors.append(f"提供商 'SILICONFOLW' 存在拼写错误,应为 'SILICONFLOW'") + + return errors + +def check_groups_configuration(config: Dict[str, Any]) -> List[str]: + """检查群组配置""" + errors = [] + + if "groups" not in config: + return ["缺少[groups]部分"] + + groups = config["groups"] + + # 检查talk_allowed是否为列表 + if "talk_allowed" not in groups: + errors.append("缺少groups.talk_allowed配置") + elif not isinstance(groups["talk_allowed"], list): + errors.append("groups.talk_allowed应该是一个列表") + else: + # 检查talk_allowed是否包含默认示例值123 + if 123 in groups["talk_allowed"]: + errors.append({ + "main": "groups.talk_allowed中存在默认示例值'123',请修改为真实的群号", + "details": [ + f" 当前值: {groups['talk_allowed']}", + f" '123'为示例值,需要替换为真实群号" + ] + }) + + # 检查是否存在重复的群号 + talk_allowed = groups["talk_allowed"] + duplicates = [] + seen = set() + for gid in talk_allowed: + if gid in seen and gid not in duplicates: + duplicates.append(gid) + seen.add(gid) + + if duplicates: + errors.append({ + "main": "groups.talk_allowed中存在重复的群号", + "details": [f" 重复的群号: {duplicates}"] + }) + + # 检查其他群组配置 + if "talk_frequency_down" in groups and not isinstance(groups["talk_frequency_down"], list): + errors.append("groups.talk_frequency_down应该是一个列表") + + if "ban_user_id" in groups and not isinstance(groups["ban_user_id"], list): + errors.append("groups.ban_user_id应该是一个列表") + + return errors + +def check_keywords_reaction(config: Dict[str, Any]) -> List[str]: + """检查关键词反应配置""" + errors = [] + + if "keywords_reaction" not in config: + return ["缺少[keywords_reaction]部分"] + + kr = config["keywords_reaction"] + + # 检查enable字段 + if "enable" not in kr: + errors.append("缺少keywords_reaction.enable配置") + + # 检查规则配置 + if "rules" not in kr: + errors.append("缺少keywords_reaction.rules配置") + elif not isinstance(kr["rules"], list): + errors.append("keywords_reaction.rules应该是一个列表") + else: + for i, rule in enumerate(kr["rules"]): + if "enable" not in rule: + errors.append(f"关键词规则 #{i+1} 缺少enable字段") + if "keywords" not in rule: + errors.append(f"关键词规则 #{i+1} 缺少keywords字段") + elif not isinstance(rule["keywords"], list): + errors.append(f"关键词规则 #{i+1} 的keywords应该是一个列表") + if "reaction" not in rule: + errors.append(f"关键词规则 #{i+1} 缺少reaction字段") + + return errors + +def check_willing_mode(config: Dict[str, Any]) -> List[str]: + """检查回复意愿模式配置""" + errors = [] + + if "willing" not in config: + return ["缺少[willing]部分"] + + willing = config["willing"] + + if "willing_mode" not in willing: + errors.append("缺少willing.willing_mode配置") + elif willing["willing_mode"] not in ["classical", "dynamic", "custom"]: + errors.append(f"willing.willing_mode值无效: {willing['willing_mode']}, 应为classical/dynamic/custom") + + return errors + +def check_memory_config(config: Dict[str, Any]) -> List[str]: + """检查记忆系统配置""" + errors = [] + + if "memory" not in config: + return ["缺少[memory]部分"] + + memory = config["memory"] + + # 检查必要的参数 + required_fields = [ + "build_memory_interval", "memory_compress_rate", + "forget_memory_interval", "memory_forget_time", + "memory_forget_percentage" + ] + + for field in required_fields: + if field not in memory: + errors.append(f"缺少memory.{field}配置") + + # 检查参数值的有效性 + if "memory_compress_rate" in memory and (memory["memory_compress_rate"] <= 0 or memory["memory_compress_rate"] > 1): + errors.append(f"memory.memory_compress_rate值无效: {memory['memory_compress_rate']}, 应在0-1之间") + + if "memory_forget_percentage" in memory and (memory["memory_forget_percentage"] <= 0 or memory["memory_forget_percentage"] > 1): + errors.append(f"memory.memory_forget_percentage值无效: {memory['memory_forget_percentage']}, 应在0-1之间") + + return errors + +def check_personality_config(config: Dict[str, Any]) -> List[str]: + """检查人格配置""" + errors = [] + + if "personality" not in config: + return ["缺少[personality]部分"] + + personality = config["personality"] + + # 检查prompt_personality是否存在且为数组 + if "prompt_personality" not in personality: + errors.append("缺少personality.prompt_personality配置") + elif not isinstance(personality["prompt_personality"], list): + errors.append("personality.prompt_personality应该是一个数组") + else: + # 检查数组长度 + if len(personality["prompt_personality"]) < 1: + errors.append(f"personality.prompt_personality数组长度不足,当前长度: {len(personality['prompt_personality'])}, 需要至少1项") + else: + # 模板默认值 + template_values = [ + "用一句话或几句话描述性格特点和其他特征", + "用一句话或几句话描述性格特点和其他特征", + "例如,是一个热爱国家热爱党的新时代好青年" + ] + + # 检查是否仍然使用默认模板值 + error_details = [] + for i, (current, template) in enumerate(zip(personality["prompt_personality"][:3], template_values)): + if current == template: + error_details.append({ + "main": f"personality.prompt_personality第{i+1}项仍使用默认模板值,请自定义", + "details": [ + f" 当前值: '{current}'", + f" 请不要使用模板值: '{template}'" + ] + }) + + # 将错误添加到errors列表 + for error in error_details: + errors.append(error) + + return errors + +def check_bot_config(config: Dict[str, Any]) -> List[str]: + """检查机器人基础配置""" + errors = [] + infos = [] + + if "bot" not in config: + return ["缺少[bot]部分"] + + bot = config["bot"] + + # 检查QQ号是否为默认值或测试值 + if "qq" not in bot: + errors.append("缺少bot.qq配置") + elif bot["qq"] == 1 or bot["qq"] == 123: + errors.append(f"QQ号 '{bot['qq']}' 似乎是默认值或测试值,请设置为真实的QQ号") + else: + infos.append(f"当前QQ号: {bot['qq']}") + + # 检查昵称是否设置 + if "nickname" not in bot or not bot["nickname"]: + errors.append("缺少bot.nickname配置或昵称为空") + elif bot["nickname"]: + infos.append(f"当前昵称: {bot['nickname']}") + + # 检查别名是否为列表 + if "alias_names" in bot and not isinstance(bot["alias_names"], list): + errors.append("bot.alias_names应该是一个列表") + + return errors, infos + +def format_results(all_errors): + """格式化检查结果""" + sections_errors, prob_sum_errors, prob_range_errors, model_errors, api_errors, groups_errors, kr_errors, willing_errors, memory_errors, personality_errors, bot_results = all_errors + bot_errors, bot_infos = bot_results + + if not any([sections_errors, prob_sum_errors, prob_range_errors, model_errors, api_errors, groups_errors, kr_errors, willing_errors, memory_errors, personality_errors, bot_errors]): + result = "✅ 配置文件检查通过,未发现问题。" + + # 添加机器人信息 + if bot_infos: + result += "\n\n【机器人信息】" + for info in bot_infos: + result += f"\n - {info}" + + return result + + output = [] + output.append("❌ 配置文件检查发现以下问题:") + + if sections_errors: + output.append("\n【缺失的配置段】") + for section in sections_errors: + output.append(f" - {section}") + + if prob_sum_errors: + output.append("\n【概率总和错误】(应为1.0)") + for name, value in prob_sum_errors: + output.append(f" - {name}: {value:.4f}") + + if prob_range_errors: + output.append("\n【概率值范围错误】(应在0-1之间)") + for name, value in prob_range_errors: + output.append(f" - {name}: {value}") + + if model_errors: + output.append("\n【模型配置错误】") + for error in model_errors: + output.append(f" - {error}") + + if api_errors: + output.append("\n【API提供商错误】") + for error in api_errors: + output.append(f" - {error}") + + if groups_errors: + output.append("\n【群组配置错误】") + for error in groups_errors: + if isinstance(error, dict): + output.append(f" - {error['main']}") + for detail in error['details']: + output.append(f"{detail}") + else: + output.append(f" - {error}") + + if kr_errors: + output.append("\n【关键词反应配置错误】") + for error in kr_errors: + output.append(f" - {error}") + + if willing_errors: + output.append("\n【回复意愿配置错误】") + for error in willing_errors: + output.append(f" - {error}") + + if memory_errors: + output.append("\n【记忆系统配置错误】") + for error in memory_errors: + output.append(f" - {error}") + + if personality_errors: + output.append("\n【人格配置错误】") + for error in personality_errors: + if isinstance(error, dict): + output.append(f" - {error['main']}") + for detail in error['details']: + output.append(f"{detail}") + else: + output.append(f" - {error}") + + if bot_errors: + output.append("\n【机器人基础配置错误】") + for error in bot_errors: + output.append(f" - {error}") + + # 添加机器人信息,即使有错误 + if bot_infos: + output.append("\n【机器人信息】") + for info in bot_infos: + output.append(f" - {info}") + + return "\n".join(output) + +def main(): + # 获取配置文件路径 + config_path = Path("config/bot_config.toml") + env_path = Path(".env.prod") + + if not config_path.exists(): + print(f"错误: 找不到配置文件 {config_path}") + return + + if not env_path.exists(): + print(f"警告: 找不到环境变量文件 {env_path}, 将跳过API提供商检查") + env_vars = {} + else: + env_vars = load_env_file(env_path) + + # 加载配置文件 + config = load_toml_file(config_path) + + # 运行各种检查 + sections_errors = check_required_sections(config) + prob_sum_errors = check_probability_sum(config) + prob_range_errors = check_probability_range(config) + model_errors = check_model_configurations(config, env_vars) + api_errors = check_api_providers(config, env_vars) + groups_errors = check_groups_configuration(config) + kr_errors = check_keywords_reaction(config) + willing_errors = check_willing_mode(config) + memory_errors = check_memory_config(config) + personality_errors = check_personality_config(config) + bot_results = check_bot_config(config) + + # 格式化并打印结果 + all_errors = (sections_errors, prob_sum_errors, prob_range_errors, model_errors, api_errors, groups_errors, kr_errors, willing_errors, memory_errors, personality_errors, bot_results) + result = format_results(all_errors) + print("📋 机器人配置检查结果:") + print(result) + + # 综合评估 + total_errors = 0 + + # 解包bot_results + bot_errors, _ = bot_results + + # 计算普通错误列表的长度 + for errors in [sections_errors, model_errors, api_errors, groups_errors, kr_errors, willing_errors, memory_errors, bot_errors]: + total_errors += len(errors) + + # 计算元组列表的长度(概率相关错误) + total_errors += len(prob_sum_errors) + total_errors += len(prob_range_errors) + + # 特殊处理personality_errors和groups_errors + for errors_list in [personality_errors, groups_errors]: + for error in errors_list: + if isinstance(error, dict): + # 每个字典表示一个错误,而不是每行都算一个 + total_errors += 1 + else: + total_errors += 1 + + if total_errors > 0: + print(f"\n总计发现 {total_errors} 个配置问题。") + print("\n建议:") + print("1. 修复所有错误后再运行机器人") + print("2. 特别注意拼写错误,例如不!要!写!错!别!字!!!!!") + print("3. 确保所有API提供商名称与环境变量中一致") + print("4. 检查概率值设置,确保总和为1") + else: + print("\n您的配置文件完全正确!机器人可以正常运行。") + +if __name__ == "__main__": + main() + input("\n按任意键退出...") \ No newline at end of file From ddd8ca321396e438a85b07f8fd845b8af255513e Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Tue, 25 Mar 2025 15:53:43 +0800 Subject: [PATCH 03/46] Update current_mind.py --- src/think_flow_demo/current_mind.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/think_flow_demo/current_mind.py b/src/think_flow_demo/current_mind.py index fd4ca6160..09634cf2d 100644 --- a/src/think_flow_demo/current_mind.py +++ b/src/think_flow_demo/current_mind.py @@ -40,7 +40,7 @@ class SubHeartflow: await self.do_a_thinking() print("麦麦闹情绪了") await self.judge_willing() - await asyncio.sleep(20) + await asyncio.sleep(30) async def do_a_thinking(self): print("麦麦小脑袋转起来了") @@ -109,7 +109,7 @@ class SubHeartflow: prompt += f"你现在的想法是{current_thinking_info}。" prompt += f"你现在{mood_info}。" prompt += f"现在请你思考,你想不想发言或者回复,请你输出一个数字,1-10,1表示非常不想,10表示非常想。" - prompt += f"请你用<>包裹你的回复意愿,例如输出<1>表示不想回复,输出<10>表示非常想回复。<5>表示想回复,但是需要思考一下。" + prompt += f"请你用<>包裹你的回复意愿,例如输出<1>表示不想回复,输出<10>表示非常想回复。请你考虑,你完全可以不回复" response, reasoning_content = await self.llm_model.generate_response_async(prompt) # 解析willing值 From 4e7efb4271a8b3eeaa554c2cf46a391749b1ade0 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Tue, 25 Mar 2025 17:03:39 +0800 Subject: [PATCH 04/46] =?UTF-8?q?fix=20=E7=A7=BB=E9=99=A4=E6=97=A0?= =?UTF-8?q?=E7=94=A8=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/willing/mode_custom.py | 106 ------------------------- src/think_flow_demo/offline_llm.py | 123 ----------------------------- 2 files changed, 229 deletions(-) delete mode 100644 src/think_flow_demo/offline_llm.py diff --git a/src/plugins/willing/mode_custom.py b/src/plugins/willing/mode_custom.py index a4d647ae2..e69de29bb 100644 --- a/src/plugins/willing/mode_custom.py +++ b/src/plugins/willing/mode_custom.py @@ -1,106 +0,0 @@ -import asyncio -from typing import Dict -from ..chat.chat_stream import ChatStream - - -class WillingManager: - def __init__(self): - self.chat_reply_willing: Dict[str, float] = {} # 存储每个聊天流的回复意愿 - self._decay_task = None - self._started = False - - async def _decay_reply_willing(self): - """定期衰减回复意愿""" - while True: - await asyncio.sleep(3) - for chat_id in self.chat_reply_willing: - # 每分钟衰减10%的回复意愿 - self.chat_reply_willing[chat_id] = max(0, self.chat_reply_willing[chat_id] * 0.6) - - def get_willing(self, chat_stream: ChatStream) -> float: - """获取指定聊天流的回复意愿""" - if chat_stream: - return self.chat_reply_willing.get(chat_stream.stream_id, 0) - return 0 - - def set_willing(self, chat_id: str, willing: float): - """设置指定聊天流的回复意愿""" - self.chat_reply_willing[chat_id] = willing - - async def change_reply_willing_received( - self, - chat_stream: ChatStream, - topic: str = None, - is_mentioned_bot: bool = False, - config=None, - is_emoji: bool = False, - interested_rate: float = 0, - sender_id: str = None, - ) -> float: - """改变指定聊天流的回复意愿并返回回复概率""" - chat_id = chat_stream.stream_id - current_willing = self.chat_reply_willing.get(chat_id, 0) - - if topic and current_willing < 1: - current_willing += 0.2 - elif topic: - current_willing += 0.05 - - if is_mentioned_bot and current_willing < 1.0: - current_willing += 0.9 - elif is_mentioned_bot: - current_willing += 0.05 - - if is_emoji: - current_willing *= 0.2 - - self.chat_reply_willing[chat_id] = min(current_willing, 3.0) - - reply_probability = (current_willing - 0.5) * 2 - - # 检查群组权限(如果是群聊) - if chat_stream.group_info and config: - if chat_stream.group_info.group_id not in config.talk_allowed_groups: - current_willing = 0 - reply_probability = 0 - - if chat_stream.group_info.group_id in config.talk_frequency_down_groups: - reply_probability = reply_probability / config.down_frequency_rate - - if is_mentioned_bot and sender_id == "1026294844": - reply_probability = 1 - - return reply_probability - - def change_reply_willing_sent(self, chat_stream: ChatStream): - """发送消息后降低聊天流的回复意愿""" - if chat_stream: - chat_id = chat_stream.stream_id - current_willing = self.chat_reply_willing.get(chat_id, 0) - self.chat_reply_willing[chat_id] = max(0, current_willing - 1.8) - - def change_reply_willing_not_sent(self, chat_stream: ChatStream): - """未发送消息后降低聊天流的回复意愿""" - if chat_stream: - chat_id = chat_stream.stream_id - current_willing = self.chat_reply_willing.get(chat_id, 0) - self.chat_reply_willing[chat_id] = max(0, current_willing - 0) - - def change_reply_willing_after_sent(self, chat_stream: ChatStream): - """发送消息后提高聊天流的回复意愿""" - if chat_stream: - chat_id = chat_stream.stream_id - current_willing = self.chat_reply_willing.get(chat_id, 0) - if current_willing < 1: - self.chat_reply_willing[chat_id] = min(1, current_willing + 0.4) - - async def ensure_started(self): - """确保衰减任务已启动""" - if not self._started: - if self._decay_task is None: - self._decay_task = asyncio.create_task(self._decay_reply_willing()) - self._started = True - - -# 创建全局实例 -willing_manager = WillingManager() diff --git a/src/think_flow_demo/offline_llm.py b/src/think_flow_demo/offline_llm.py deleted file mode 100644 index db51ca00f..000000000 --- a/src/think_flow_demo/offline_llm.py +++ /dev/null @@ -1,123 +0,0 @@ -import asyncio -import os -import time -from typing import Tuple, Union - -import aiohttp -import requests -from src.common.logger import get_module_logger - -logger = get_module_logger("offline_llm") - - -class LLMModel: - def __init__(self, model_name="Pro/deepseek-ai/DeepSeek-V3", **kwargs): - self.model_name = model_name - self.params = kwargs - self.api_key = os.getenv("SILICONFLOW_KEY") - self.base_url = os.getenv("SILICONFLOW_BASE_URL") - - if not self.api_key or not self.base_url: - raise ValueError("环境变量未正确加载:SILICONFLOW_KEY 或 SILICONFLOW_BASE_URL 未设置") - - logger.info(f"API URL: {self.base_url}") # 使用 logger 记录 base_url - - def generate_response(self, prompt: str) -> Union[str, Tuple[str, str]]: - """根据输入的提示生成模型的响应""" - headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"} - - # 构建请求体 - data = { - "model": self.model_name, - "messages": [{"role": "user", "content": prompt}], - "temperature": 0.5, - **self.params, - } - - # 发送请求到完整的 chat/completions 端点 - api_url = f"{self.base_url.rstrip('/')}/chat/completions" - logger.info(f"Request URL: {api_url}") # 记录请求的 URL - - max_retries = 3 - base_wait_time = 15 # 基础等待时间(秒) - - for retry in range(max_retries): - try: - response = requests.post(api_url, headers=headers, json=data) - - if response.status_code == 429: - wait_time = base_wait_time * (2**retry) # 指数退避 - logger.warning(f"遇到请求限制(429),等待{wait_time}秒后重试...") - time.sleep(wait_time) - continue - - response.raise_for_status() # 检查其他响应状态 - - result = response.json() - if "choices" in result and len(result["choices"]) > 0: - content = result["choices"][0]["message"]["content"] - reasoning_content = result["choices"][0]["message"].get("reasoning_content", "") - return content, reasoning_content - return "没有返回结果", "" - - except Exception as e: - if retry < max_retries - 1: # 如果还有重试机会 - wait_time = base_wait_time * (2**retry) - logger.error(f"[回复]请求失败,等待{wait_time}秒后重试... 错误: {str(e)}") - time.sleep(wait_time) - else: - logger.error(f"请求失败: {str(e)}") - return f"请求失败: {str(e)}", "" - - logger.error("达到最大重试次数,请求仍然失败") - return "达到最大重试次数,请求仍然失败", "" - - async def generate_response_async(self, prompt: str) -> Union[str, Tuple[str, str]]: - """异步方式根据输入的提示生成模型的响应""" - headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"} - - # 构建请求体 - data = { - "model": self.model_name, - "messages": [{"role": "user", "content": prompt}], - "temperature": 0.5, - **self.params, - } - - # 发送请求到完整的 chat/completions 端点 - api_url = f"{self.base_url.rstrip('/')}/chat/completions" - logger.info(f"Request URL: {api_url}") # 记录请求的 URL - - max_retries = 3 - base_wait_time = 15 - - async with aiohttp.ClientSession() as session: - for retry in range(max_retries): - try: - async with session.post(api_url, headers=headers, json=data) as response: - if response.status == 429: - wait_time = base_wait_time * (2**retry) # 指数退避 - logger.warning(f"遇到请求限制(429),等待{wait_time}秒后重试...") - await asyncio.sleep(wait_time) - continue - - response.raise_for_status() # 检查其他响应状态 - - result = await response.json() - if "choices" in result and len(result["choices"]) > 0: - content = result["choices"][0]["message"]["content"] - reasoning_content = result["choices"][0]["message"].get("reasoning_content", "") - return content, reasoning_content - return "没有返回结果", "" - - except Exception as e: - if retry < max_retries - 1: # 如果还有重试机会 - wait_time = base_wait_time * (2**retry) - logger.error(f"[回复]请求失败,等待{wait_time}秒后重试... 错误: {str(e)}") - await asyncio.sleep(wait_time) - else: - logger.error(f"请求失败: {str(e)}") - return f"请求失败: {str(e)}", "" - - logger.error("达到最大重试次数,请求仍然失败") - return "达到最大重试次数,请求仍然失败", "" From c220f4c79e45265ce83ccd0e678d8bd232c7168d Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Tue, 25 Mar 2025 21:59:15 +0800 Subject: [PATCH 05/46] =?UTF-8?q?better=20=E9=85=8D=E7=BD=AE=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=95=B4=E7=90=86,=E9=98=B2=E6=AD=A2=E7=9C=BC?= =?UTF-8?q?=E8=8A=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 123 --- config/bot_config_test.toml | 179 +++++++++++++++++++++++++ src/plugins/chat/__init__.py | 4 +- src/plugins/chat/bot.py | 3 - src/plugins/chat/config.py | 208 +++++++++++++++-------------- src/plugins/chat/emoji_manager.py | 8 +- src/plugins/willing/mode_custom.py | 102 ++++++++++++++ template/bot_config_template.toml | 90 +++++++------ 7 files changed, 446 insertions(+), 148 deletions(-) create mode 100644 config/bot_config_test.toml diff --git a/config/bot_config_test.toml b/config/bot_config_test.toml new file mode 100644 index 000000000..dd01bfded --- /dev/null +++ b/config/bot_config_test.toml @@ -0,0 +1,179 @@ +[inner] +version = "0.0.10" + +[mai_version] +version = "0.6.0" +version-fix = "snapshot-1" + +#以下是给开发人员阅读的,一般用户不需要阅读 +#如果你想要修改配置文件,请在修改后将version的值进行变更 +#如果新增项目,请在BotConfig类下新增相应的变量 +#1.如果你修改的是[]层级项目,例如你新增了 [memory],那么请在config.py的 load_config函数中的include_configs字典中新增"内容":{ +#"func":memory, +#"support":">=0.0.0", #新的版本号 +#"necessary":False #是否必须 +#} +#2.如果你修改的是[]下的项目,例如你新增了[memory]下的 memory_ban_words ,那么请在config.py的 load_config函数中的 memory函数下新增版本判断: + # if config.INNER_VERSION in SpecifierSet(">=0.0.2"): + # config.memory_ban_words = set(memory_config.get("memory_ban_words", [])) + +[bot] +qq = 2814567326 +nickname = "麦麦" +alias_names = ['牢麦', '麦叠', '哈基麦'] + +[personality] +prompt_personality = ['曾经是一个学习地质的女大学生,现在学习心理学和脑科学,你会刷贴吧', '是一个女大学生,你有黑色头发,你会刷小红书', '是一个女大学生,你会刷b站,对ACG文化感兴趣'] +personality_1_probability = 0.7 # 第一种人格出现概率 +personality_2_probability = 0.1 # 第二种人格出现概率 +personality_3_probability = 0.2 # 第三种人格出现概率,请确保三个概率相加等于1 +prompt_schedule = "一个曾经学习地质,现在学习心理学和脑科学的女大学生,喜欢刷qq,贴吧,知乎和小红书" + +[message] +min_text_length = 2 # 与麦麦聊天时麦麦只会回答文本大于等于此数的消息 +max_context_size = 10 # 麦麦获得的上文数量 +emoji_chance = 0.2 # 麦麦使用表情包的概率 +thinking_timeout = 100 # 麦麦思考时间 + +response_willing_amplifier = 1 # 麦麦回复意愿放大系数,一般为1 +response_interested_rate_amplifier = 1 # 麦麦回复兴趣度放大系数,听到记忆里的内容时放大系数 +down_frequency_rate = 2 # 降低回复频率的群组回复意愿降低系数 +ban_words = [] + +ban_msgs_regex = [] + +[emoji] +check_interval = 120 # 检查表情包的时间间隔 +register_interval = 10 # 注册表情包的时间间隔 +auto_save = true # 自动偷表情包 +enable_check = false # 是否启用表情包过滤 +check_prompt = "符合公序良俗" # 表情包过滤要求 + +[cq_code] +enable_pic_translate = false + +[response] +model_r1_probability = 0.5 # 麦麦回答时选择主要回复模型1 模型的概率 +model_v3_probability = 0.5 # 麦麦回答时选择次要回复模型2 模型的概率 +model_r1_distill_probability = 0 # 麦麦回答时选择次要回复模型3 模型的概率 +max_response_length = 1024 # 麦麦回答的最大token数 + +[willing] +willing_mode = "classical" # 回复意愿模式 经典模式 +# willing_mode = "dynamic" # 动态模式(可能不兼容) +# willing_mode = "custom" # 自定义模式(可自行调整 + +[memory] +build_memory_interval = 3000 # 记忆构建间隔 单位秒 间隔越低,麦麦学习越多,但是冗余信息也会增多 +build_memory_distribution = [4, 4, 0.6, 48, 36, 0.4] # 记忆构建分布,参数:分布1均值,标准差,权重,分布2均值,标准差,权重 +build_memory_sample_num = 10 # 采样数量,数值越高记忆采样次数越多 +build_memory_sample_length = 30 # 采样长度,数值越高一段记忆内容越丰富 +memory_compress_rate = 0.1 # 记忆压缩率 控制记忆精简程度 建议保持默认,调高可以获得更多信息,但是冗余信息也会增多 + +forget_memory_interval = 300 # 记忆遗忘间隔 单位秒 间隔越低,麦麦遗忘越频繁,记忆更精简,但更难学习 +memory_forget_time = 24 #多长时间后的记忆会被遗忘 单位小时 +memory_forget_percentage = 0.005 # 记忆遗忘比例 控制记忆遗忘程度 越大遗忘越多 建议保持默认 + + +memory_ban_words = ['表情包', '图片', '回复', '聊天记录'] + +[mood] +mood_update_interval = 1.0 # 情绪更新间隔 单位秒 +mood_decay_rate = 0.95 # 情绪衰减率 +mood_intensity_factor = 1.0 # 情绪强度因子 + +[keywords_reaction] # 针对某个关键词作出反应 +enable = true # 关键词反应功能的总开关 + +[[keywords_reaction.rules]] +enable = true +keywords = [ "人机", "bot", "机器", "入机", "robot", "机器人",] +reaction = "有人说你是人机或者机器人,否定这一事实,攻击或反问他。注意,只有认为你是机器人才回复,否则不要否认" + +[[keywords_reaction.rules]] +enable = false +keywords = [ "测试关键词回复", "test", "",] +reaction = "回答“测试成功”" + +[chinese_typo] +enable = true # 是否启用中文错别字生成器 +error_rate=0.01 # 单字替换概率 +min_freq=7 # 最小字频阈值 +tone_error_rate=0.3 # 声调错误概率 +word_replace_rate=0.01 # 整词替换概率 + +[others] +enable_kuuki_read = true # 是否启用读空气功能 +enable_friend_chat = true # 是否启用好友聊天 + +[groups] +talk_allowed = [571780722,1022489779,534940728, 192194125, 851345375, 739044565, 766798517, 1030993430, 435591861, 708847644, 591693379, 571780722, 1028699246, 571780722, 1015816696] #可以回复消息的群 +talk_frequency_down = [1022489779, 571780722] #降低回复频率的群 +ban_user_id = [3488737411, 2732836727, 3878664193, 3799953254] #禁止回复和读取消息的QQ号 + +[remote] #发送统计信息,主要是看全球有多少只麦麦 +enable = true + +#下面的模型若使用硅基流动则不需要更改,使用ds官方则改成.env.prod自定义的宏,使用自定义模型则选择定位相似的模型自己填写 + +#推理模型 + +[model.llm_reasoning] #回复模型1 主要回复模型 +# name = "Pro/deepseek-ai/DeepSeek-R1" +name = "Qwen/QwQ-32B" +provider = "SILICONFLOW" +pri_in = 1.0 #模型的输入价格(非必填,可以记录消耗) +pri_out = 4.0 #模型的输出价格(非必填,可以记录消耗) + +[model.llm_reasoning_minor] #回复模型3 次要回复模型 +name = "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B" +provider = "SILICONFLOW" +pri_in = 1.26 #模型的输入价格(非必填,可以记录消耗) +pri_out = 1.26 #模型的输出价格(非必填,可以记录消耗) + +#非推理模型 + +[model.llm_normal] #V3 回复模型2 次要回复模型 +name = "Qwen/Qwen2.5-32B-Instruct" +provider = "SILICONFLOW" +pri_in = 1.26 #模型的输入价格(非必填,可以记录消耗) +pri_out = 1.26 #模型的输出价格(非必填,可以记录消耗) + +[model.llm_emotion_judge] #表情包判断 +name = "Qwen/Qwen2.5-14B-Instruct" +provider = "SILICONFLOW" +pri_in = 0.7 +pri_out = 0.7 + +[model.llm_topic_judge] #记忆主题判断:建议使用qwen2.5 7b +name = "Pro/Qwen/Qwen2.5-7B-Instruct" +# name = "Qwen/Qwen2-1.5B-Instruct" +provider = "SILICONFLOW" +pri_in = 0.35 +pri_out = 0.35 + +[model.llm_summary_by_topic] #概括模型,建议使用qwen2.5 32b 及以上 +name = "Qwen/Qwen2.5-32B-Instruct" +provider = "SILICONFLOW" +pri_in = 1.26 +pri_out = 1.26 + +[model.moderation] #内容审核,开发中 +name = "" +provider = "SILICONFLOW" +pri_in = 1.0 +pri_out = 2.0 + +# 识图模型 + +[model.vlm] #图像识别 +name = "Pro/Qwen/Qwen2.5-VL-7B-Instruct" +provider = "SILICONFLOW" +pri_in = 0.35 +pri_out = 0.35 + +#嵌入模型 + +[model.embedding] #嵌入 +name = "BAAI/bge-m3" +provider = "SILICONFLOW" diff --git a/src/plugins/chat/__init__.py b/src/plugins/chat/__init__.py index c4c85bcd4..713f1d375 100644 --- a/src/plugins/chat/__init__.py +++ b/src/plugins/chat/__init__.py @@ -77,7 +77,7 @@ async def start_background_tasks(): logger.success("心流系统启动成功") # 只启动表情包管理任务 - asyncio.create_task(emoji_manager.start_periodic_check(interval_MINS=global_config.EMOJI_CHECK_INTERVAL)) + asyncio.create_task(emoji_manager.start_periodic_check()) await bot_schedule.initialize() bot_schedule.print_schedule() @@ -105,7 +105,7 @@ async def _(bot: Bot): _message_manager_started = True logger.success("-----------消息处理器已启动!-----------") - asyncio.create_task(emoji_manager._periodic_scan(interval_MINS=global_config.EMOJI_REGISTER_INTERVAL)) + asyncio.create_task(emoji_manager._periodic_scan()) logger.success("-----------开始偷表情包!-----------") asyncio.create_task(chat_manager._initialize()) asyncio.create_task(chat_manager._auto_save_task()) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 57c387c09..a9e76648a 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -57,9 +57,6 @@ class ChatBot: self.mood_manager = MoodManager.get_instance() # 获取情绪管理器单例 self.mood_manager.start_mood_update() # 启动情绪更新 - self.emoji_chance = 0.2 # 发送表情包的基础概率 - # self.message_streams = MessageStreamContainer() - async def _ensure_started(self): """确保所有任务已启动""" if not self._started: diff --git a/src/plugins/chat/config.py b/src/plugins/chat/config.py index 09ebe3520..b16af9137 100644 --- a/src/plugins/chat/config.py +++ b/src/plugins/chat/config.py @@ -17,40 +17,99 @@ class BotConfig: """机器人配置类""" INNER_VERSION: Version = None - - BOT_QQ: Optional[int] = 1 + MAI_VERSION: Version = None + + # bot + BOT_QQ: Optional[int] = 114514 BOT_NICKNAME: Optional[str] = None BOT_ALIAS_NAMES: List[str] = field(default_factory=list) # 别名,可以通过这个叫它 - - # 消息处理相关配置 - MIN_TEXT_LENGTH: int = 2 # 最小处理文本长度 - MAX_CONTEXT_SIZE: int = 15 # 上下文最大消息数 - emoji_chance: float = 0.2 # 发送表情包的基础概率 - - ENABLE_PIC_TRANSLATE: bool = True # 是否启用图片翻译 - + + # group talk_allowed_groups = set() talk_frequency_down_groups = set() - thinking_timeout: int = 100 # 思考时间 + ban_user_id = set() + + #personality + PROMPT_PERSONALITY = [ + "用一句话或几句话描述性格特点和其他特征", + "例如,是一个热爱国家热爱党的新时代好青年", + "例如,曾经是一个学习地质的女大学生,现在学习心理学和脑科学,你会刷贴吧" + ] + PERSONALITY_1: float = 0.6 # 第一种人格概率 + PERSONALITY_2: float = 0.3 # 第二种人格概率 + PERSONALITY_3: float = 0.1 # 第三种人格概率 + + # schedule + ENABLE_SCHEDULE_GEN: bool = False # 是否启用日程生成 + PROMPT_SCHEDULE_GEN = "无日程" + # message + MAX_CONTEXT_SIZE: int = 15 # 上下文最大消息数 + emoji_chance: float = 0.2 # 发送表情包的基础概率 + thinking_timeout: int = 120 # 思考时间 + max_response_length: int = 1024 # 最大回复长度 + + ban_words = set() + ban_msgs_regex = set() + + # willing + willing_mode: str = "classical" # 意愿模式 response_willing_amplifier: float = 1.0 # 回复意愿放大系数 response_interested_rate_amplifier: float = 1.0 # 回复兴趣度放大系数 - down_frequency_rate: float = 3.5 # 降低回复频率的群组回复意愿降低系数 - - ban_user_id = set() + down_frequency_rate: float = 3 # 降低回复频率的群组回复意愿降低系数 + + # response + MODEL_R1_PROBABILITY: float = 0.8 # R1模型概率 + MODEL_V3_PROBABILITY: float = 0.1 # V3模型概率 + MODEL_R1_DISTILL_PROBABILITY: float = 0.1 # R1蒸馏模型概率 + # emoji EMOJI_CHECK_INTERVAL: int = 120 # 表情包检查间隔(分钟) EMOJI_REGISTER_INTERVAL: int = 10 # 表情包注册间隔(分钟) EMOJI_SAVE: bool = True # 偷表情包 EMOJI_CHECK: bool = False # 是否开启过滤 EMOJI_CHECK_PROMPT: str = "符合公序良俗" # 表情包过滤要求 - ban_words = set() - ban_msgs_regex = set() + # memory + build_memory_interval: int = 600 # 记忆构建间隔(秒) + memory_build_distribution: list = field( + default_factory=lambda: [4,2,0.6,24,8,0.4] + ) # 记忆构建分布,参数:分布1均值,标准差,权重,分布2均值,标准差,权重 + build_memory_sample_num: int = 10 # 记忆构建采样数量 + build_memory_sample_length: int = 20 # 记忆构建采样长度 + memory_compress_rate: float = 0.1 # 记忆压缩率 + + forget_memory_interval: int = 600 # 记忆遗忘间隔(秒) + memory_forget_time: int = 24 # 记忆遗忘时间(小时) + memory_forget_percentage: float = 0.01 # 记忆遗忘比例 + + memory_ban_words: list = field( + default_factory=lambda: ["表情包", "图片", "回复", "聊天记录"] + ) # 添加新的配置项默认值 - max_response_length: int = 1024 # 最大回复长度 + # mood + mood_update_interval: float = 1.0 # 情绪更新间隔 单位秒 + mood_decay_rate: float = 0.95 # 情绪衰减率 + mood_intensity_factor: float = 0.7 # 情绪强度因子 + + # keywords + keywords_reaction_rules = [] # 关键词回复规则 + + # chinese_typo + chinese_typo_enable = True # 是否启用中文错别字生成器 + chinese_typo_error_rate = 0.03 # 单字替换概率 + chinese_typo_min_freq = 7 # 最小字频阈值 + chinese_typo_tone_error_rate = 0.2 # 声调错误概率 + chinese_typo_word_replace_rate = 0.02 # 整词替换概率 - remote_enable: bool = False # 是否启用远程控制 + # remote + remote_enable: bool = True # 是否启用远程控制 + + # experimental + enable_friend_chat: bool = False # 是否启用好友聊天 + enable_think_flow: bool = False # 是否启用思考流程 + + # 模型配置 llm_reasoning: Dict[str, str] = field(default_factory=lambda: {}) @@ -59,61 +118,13 @@ class BotConfig: llm_topic_judge: Dict[str, str] = field(default_factory=lambda: {}) llm_summary_by_topic: Dict[str, str] = field(default_factory=lambda: {}) llm_emotion_judge: Dict[str, str] = field(default_factory=lambda: {}) - llm_outer_world: Dict[str, str] = field(default_factory=lambda: {}) embedding: Dict[str, str] = field(default_factory=lambda: {}) vlm: Dict[str, str] = field(default_factory=lambda: {}) moderation: Dict[str, str] = field(default_factory=lambda: {}) - MODEL_R1_PROBABILITY: float = 0.8 # R1模型概率 - MODEL_V3_PROBABILITY: float = 0.1 # V3模型概率 - MODEL_R1_DISTILL_PROBABILITY: float = 0.1 # R1蒸馏模型概率 + # 实验性 + llm_outer_world: Dict[str, str] = field(default_factory=lambda: {}) - # enable_advance_output: bool = False # 是否启用高级输出 - enable_kuuki_read: bool = True # 是否启用读空气功能 - # enable_debug_output: bool = False # 是否启用调试输出 - enable_friend_chat: bool = False # 是否启用好友聊天 - - mood_update_interval: float = 1.0 # 情绪更新间隔 单位秒 - mood_decay_rate: float = 0.95 # 情绪衰减率 - mood_intensity_factor: float = 0.7 # 情绪强度因子 - - willing_mode: str = "classical" # 意愿模式 - - keywords_reaction_rules = [] # 关键词回复规则 - - chinese_typo_enable = True # 是否启用中文错别字生成器 - chinese_typo_error_rate = 0.03 # 单字替换概率 - chinese_typo_min_freq = 7 # 最小字频阈值 - chinese_typo_tone_error_rate = 0.2 # 声调错误概率 - chinese_typo_word_replace_rate = 0.02 # 整词替换概率 - - # 默认人设 - PROMPT_PERSONALITY = [ - "曾经是一个学习地质的女大学生,现在学习心理学和脑科学,你会刷贴吧", - "是一个女大学生,你有黑色头发,你会刷小红书", - "是一个女大学生,你会刷b站,对ACG文化感兴趣", - ] - - PROMPT_SCHEDULE_GEN = "一个曾经学习地质,现在学习心理学和脑科学的女大学生,喜欢刷qq,贴吧,知乎和小红书" - - PERSONALITY_1: float = 0.6 # 第一种人格概率 - PERSONALITY_2: float = 0.3 # 第二种人格概率 - PERSONALITY_3: float = 0.1 # 第三种人格概率 - - build_memory_interval: int = 600 # 记忆构建间隔(秒) - - forget_memory_interval: int = 600 # 记忆遗忘间隔(秒) - memory_forget_time: int = 24 # 记忆遗忘时间(小时) - memory_forget_percentage: float = 0.01 # 记忆遗忘比例 - memory_compress_rate: float = 0.1 # 记忆压缩率 - build_memory_sample_num: int = 10 # 记忆构建采样数量 - build_memory_sample_length: int = 20 # 记忆构建采样长度 - memory_build_distribution: list = field( - default_factory=lambda: [4,2,0.6,24,8,0.4] - ) # 记忆构建分布,参数:分布1均值,标准差,权重,分布2均值,标准差,权重 - memory_ban_words: list = field( - default_factory=lambda: ["表情包", "图片", "回复", "聊天记录"] - ) # 添加新的配置项默认值 @staticmethod def get_config_dir() -> str: @@ -184,13 +195,17 @@ class BotConfig: if len(personality) >= 2: logger.debug(f"载入自定义人格:{personality}") config.PROMPT_PERSONALITY = personality_config.get("prompt_personality", config.PROMPT_PERSONALITY) - logger.info(f"载入自定义日程prompt:{personality_config.get('prompt_schedule', config.PROMPT_SCHEDULE_GEN)}") - config.PROMPT_SCHEDULE_GEN = personality_config.get("prompt_schedule", config.PROMPT_SCHEDULE_GEN) - + if config.INNER_VERSION in SpecifierSet(">=0.0.2"): config.PERSONALITY_1 = personality_config.get("personality_1_probability", config.PERSONALITY_1) config.PERSONALITY_2 = personality_config.get("personality_2_probability", config.PERSONALITY_2) config.PERSONALITY_3 = personality_config.get("personality_3_probability", config.PERSONALITY_3) + + def schedule(parent: dict): + schedule_config = parent["schedule"] + config.ENABLE_SCHEDULE_GEN = schedule_config.get("enable_schedule_gen", config.ENABLE_SCHEDULE_GEN) + config.PROMPT_SCHEDULE_GEN = schedule_config.get("prompt_schedule_gen", config.PROMPT_SCHEDULE_GEN) + logger.info(f"载入自定义日程prompt:{schedule_config.get('prompt_schedule_gen', config.PROMPT_SCHEDULE_GEN)}") def emoji(parent: dict): emoji_config = parent["emoji"] @@ -200,10 +215,6 @@ class BotConfig: config.EMOJI_SAVE = emoji_config.get("auto_save", config.EMOJI_SAVE) config.EMOJI_CHECK = emoji_config.get("enable_check", config.EMOJI_CHECK) - def cq_code(parent: dict): - cq_code_config = parent["cq_code"] - config.ENABLE_PIC_TRANSLATE = cq_code_config.get("enable_pic_translate", config.ENABLE_PIC_TRANSLATE) - def bot(parent: dict): # 机器人基础配置 bot_config = parent["bot"] @@ -226,6 +237,11 @@ class BotConfig: def willing(parent: dict): willing_config = parent["willing"] config.willing_mode = willing_config.get("willing_mode", config.willing_mode) + + if config.INNER_VERSION in SpecifierSet(">=0.0.11"): + config.response_willing_amplifier = willing_config.get("response_willing_amplifier", config.response_willing_amplifier) + config.response_interested_rate_amplifier = willing_config.get("response_interested_rate_amplifier", config.response_interested_rate_amplifier) + config.down_frequency_rate = willing_config.get("down_frequency_rate", config.down_frequency_rate) def model(parent: dict): # 加载模型配置 @@ -238,10 +254,10 @@ class BotConfig: "llm_topic_judge", "llm_summary_by_topic", "llm_emotion_judge", - "llm_outer_world", "vlm", "embedding", "moderation", + "llm_outer_world", ] for item in config_list: @@ -282,12 +298,11 @@ class BotConfig: # 如果 列表中的项目在 model_config 中,利用反射来设置对应项目 setattr(config, item, cfg_target) else: - logger.error(f"模型 {item} 在config中不存在,请检查") - raise KeyError(f"模型 {item} 在config中不存在,请检查") + logger.error(f"模型 {item} 在config中不存在,请检查,或尝试更新配置文件") + raise KeyError(f"模型 {item} 在config中不存在,请检查,或尝试更新配置文件") def message(parent: dict): msg_config = parent["message"] - config.MIN_TEXT_LENGTH = msg_config.get("min_text_length", config.MIN_TEXT_LENGTH) config.MAX_CONTEXT_SIZE = msg_config.get("max_context_size", config.MAX_CONTEXT_SIZE) config.emoji_chance = msg_config.get("emoji_chance", config.emoji_chance) config.ban_words = msg_config.get("ban_words", config.ban_words) @@ -304,7 +319,9 @@ class BotConfig: if config.INNER_VERSION in SpecifierSet(">=0.0.6"): config.ban_msgs_regex = msg_config.get("ban_msgs_regex", config.ban_msgs_regex) - + + if config.INNER_VERSION in SpecifierSet(">=0.0.11"): + config.max_response_length = msg_config.get("max_response_length", config.max_response_length) def memory(parent: dict): memory_config = parent["memory"] config.build_memory_interval = memory_config.get("build_memory_interval", config.build_memory_interval) @@ -368,13 +385,9 @@ class BotConfig: config.talk_frequency_down_groups = set(groups_config.get("talk_frequency_down", [])) config.ban_user_id = set(groups_config.get("ban_user_id", [])) - def others(parent: dict): - others_config = parent["others"] - # config.enable_advance_output = others_config.get("enable_advance_output", config.enable_advance_output) - config.enable_kuuki_read = others_config.get("enable_kuuki_read", config.enable_kuuki_read) - if config.INNER_VERSION in SpecifierSet(">=0.0.7"): - # config.enable_debug_output = others_config.get("enable_debug_output", config.enable_debug_output) - config.enable_friend_chat = others_config.get("enable_friend_chat", config.enable_friend_chat) + def experimental(parent: dict): + experimental_config = parent["experimental"] + config.enable_friend_chat = experimental_config.get("enable_friend_chat", config.enable_friend_chat) # 版本表达式:>=1.0.0,<2.0.0 # 允许字段:func: method, support: str, notice: str, necessary: bool @@ -382,21 +395,21 @@ class BotConfig: # 例如:"notice": "personality 将在 1.3.2 后被移除",那么在有效版本中的用户就会虽然可以 # 正常执行程序,但是会看到这条自定义提示 include_configs = { - "personality": {"func": personality, "support": ">=0.0.0"}, - "emoji": {"func": emoji, "support": ">=0.0.0"}, - "cq_code": {"func": cq_code, "support": ">=0.0.0"}, "bot": {"func": bot, "support": ">=0.0.0"}, - "response": {"func": response, "support": ">=0.0.0"}, - "willing": {"func": willing, "support": ">=0.0.9", "necessary": False}, - "model": {"func": model, "support": ">=0.0.0"}, + "groups": {"func": groups, "support": ">=0.0.0"}, + "personality": {"func": personality, "support": ">=0.0.0"}, + "schedule": {"func": schedule, "support": ">=0.0.11", "necessary": False}, "message": {"func": message, "support": ">=0.0.0"}, + "willing": {"func": willing, "support": ">=0.0.9", "necessary": False}, + "emoji": {"func": emoji, "support": ">=0.0.0"}, + "response": {"func": response, "support": ">=0.0.0"}, + "model": {"func": model, "support": ">=0.0.0"}, "memory": {"func": memory, "support": ">=0.0.0", "necessary": False}, "mood": {"func": mood, "support": ">=0.0.0"}, "remote": {"func": remote, "support": ">=0.0.10", "necessary": False}, "keywords_reaction": {"func": keywords_reaction, "support": ">=0.0.2", "necessary": False}, "chinese_typo": {"func": chinese_typo, "support": ">=0.0.3", "necessary": False}, - "groups": {"func": groups, "support": ">=0.0.0"}, - "others": {"func": others, "support": ">=0.0.0"}, + "experimental": {"func": experimental, "support": ">=0.0.11", "necessary": False}, } # 原地修改,将 字符串版本表达式 转换成 版本对象 @@ -454,14 +467,13 @@ class BotConfig: # 获取配置文件路径 bot_config_floder_path = BotConfig.get_config_dir() -logger.debug(f"正在品鉴配置文件目录: {bot_config_floder_path}") +logger.info(f"正在品鉴配置文件目录: {bot_config_floder_path}") bot_config_path = os.path.join(bot_config_floder_path, "bot_config.toml") if os.path.exists(bot_config_path): # 如果开发环境配置文件不存在,则使用默认配置文件 - logger.debug(f"异常的新鲜,异常的美味: {bot_config_path}") - logger.info("使用bot配置文件") + logger.info(f"异常的新鲜,异常的美味: {bot_config_path}") else: # 配置文件不存在 logger.error("配置文件不存在,请检查路径: {bot_config_path}") diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py index 683a37736..20a5c3b1b 100644 --- a/src/plugins/chat/emoji_manager.py +++ b/src/plugins/chat/emoji_manager.py @@ -340,12 +340,12 @@ class EmojiManager: except Exception: logger.exception("[错误] 扫描表情包失败") - async def _periodic_scan(self, interval_MINS: int = 10): + async def _periodic_scan(self): """定期扫描新表情包""" while True: logger.info("[扫描] 开始扫描新表情包...") await self.scan_new_emojis() - await asyncio.sleep(interval_MINS * 60) # 每600秒扫描一次 + await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60) def check_emoji_file_integrity(self): """检查表情包文件完整性 @@ -418,10 +418,10 @@ class EmojiManager: logger.error(f"[错误] 检查表情包完整性失败: {str(e)}") logger.error(traceback.format_exc()) - async def start_periodic_check(self, interval_MINS: int = 120): + async def start_periodic_check(self): while True: self.check_emoji_file_integrity() - await asyncio.sleep(interval_MINS * 60) + await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60) # 创建全局单例 diff --git a/src/plugins/willing/mode_custom.py b/src/plugins/willing/mode_custom.py index e69de29bb..a131b576d 100644 --- a/src/plugins/willing/mode_custom.py +++ b/src/plugins/willing/mode_custom.py @@ -0,0 +1,102 @@ +import asyncio +from typing import Dict +from ..chat.chat_stream import ChatStream + + +class WillingManager: + def __init__(self): + self.chat_reply_willing: Dict[str, float] = {} # 存储每个聊天流的回复意愿 + self._decay_task = None + self._started = False + + async def _decay_reply_willing(self): + """定期衰减回复意愿""" + while True: + await asyncio.sleep(1) + for chat_id in self.chat_reply_willing: + self.chat_reply_willing[chat_id] = max(0, self.chat_reply_willing[chat_id] * 0.9) + + def get_willing(self, chat_stream: ChatStream) -> float: + """获取指定聊天流的回复意愿""" + if chat_stream: + return self.chat_reply_willing.get(chat_stream.stream_id, 0) + return 0 + + def set_willing(self, chat_id: str, willing: float): + """设置指定聊天流的回复意愿""" + self.chat_reply_willing[chat_id] = willing + + async def change_reply_willing_received( + self, + chat_stream: ChatStream, + is_mentioned_bot: bool = False, + config=None, + is_emoji: bool = False, + interested_rate: float = 0, + sender_id: str = None, + ) -> float: + """改变指定聊天流的回复意愿并返回回复概率""" + chat_id = chat_stream.stream_id + current_willing = self.chat_reply_willing.get(chat_id, 0) + + interested_rate = interested_rate * config.response_interested_rate_amplifier + + + if interested_rate > 0.4: + current_willing += interested_rate - 0.3 + + if is_mentioned_bot and current_willing < 1.0: + current_willing += 1 + elif is_mentioned_bot: + current_willing += 0.05 + + if is_emoji: + current_willing *= 0.2 + + self.chat_reply_willing[chat_id] = min(current_willing, 3.0) + + reply_probability = min(max((current_willing - 0.5), 0.01) * config.response_willing_amplifier * 2, 1) + + # 检查群组权限(如果是群聊) + if chat_stream.group_info and config: + if chat_stream.group_info.group_id not in config.talk_allowed_groups: + current_willing = 0 + reply_probability = 0 + + if chat_stream.group_info.group_id in config.talk_frequency_down_groups: + reply_probability = reply_probability / config.down_frequency_rate + + return reply_probability + + def change_reply_willing_sent(self, chat_stream: ChatStream): + """发送消息后降低聊天流的回复意愿""" + if chat_stream: + chat_id = chat_stream.stream_id + current_willing = self.chat_reply_willing.get(chat_id, 0) + self.chat_reply_willing[chat_id] = max(0, current_willing - 1.8) + + def change_reply_willing_not_sent(self, chat_stream: ChatStream): + """未发送消息后降低聊天流的回复意愿""" + if chat_stream: + chat_id = chat_stream.stream_id + current_willing = self.chat_reply_willing.get(chat_id, 0) + self.chat_reply_willing[chat_id] = max(0, current_willing - 0) + + def change_reply_willing_after_sent(self, chat_stream: ChatStream): + """发送消息后提高聊天流的回复意愿""" + if chat_stream: + chat_id = chat_stream.stream_id + current_willing = self.chat_reply_willing.get(chat_id, 0) + if current_willing < 1: + self.chat_reply_willing[chat_id] = min(1, current_willing + 0.4) + + async def ensure_started(self): + """确保衰减任务已启动""" + if not self._started: + if self._decay_task is None: + self._decay_task = asyncio.create_task(self._decay_reply_willing()) + self._started = True + + +# 创建全局实例 +willing_manager = WillingManager() diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index bf7118d12..dcd3403af 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,6 +1,10 @@ [inner] version = "0.0.11" +[mai_version] +version = "0.6.0" +version-fix = "snapshot-1" + #以下是给开发人员阅读的,一般用户不需要阅读 #如果你想要修改配置文件,请在修改后将version的值进行变更 #如果新增项目,请在BotConfig类下新增相应的变量 @@ -14,30 +18,37 @@ version = "0.0.11" # config.memory_ban_words = set(memory_config.get("memory_ban_words", [])) [bot] -qq = 123 +qq = 114514 nickname = "麦麦" alias_names = ["麦叠", "牢麦"] +[groups] +talk_allowed = [ + 123, + 123, +] #可以回复消息的群号码 +talk_frequency_down = [] #降低回复频率的群号码 +ban_user_id = [] #禁止回复和读取消息的QQ号 + [personality] prompt_personality = [ "用一句话或几句话描述性格特点和其他特征", - "用一句话或几句话描述性格特点和其他特征", - "例如,是一个热爱国家热爱党的新时代好青年" + "例如,是一个热爱国家热爱党的新时代好青年", + "例如,曾经是一个学习地质的女大学生,现在学习心理学和脑科学,你会刷贴吧" ] personality_1_probability = 0.7 # 第一种人格出现概率 -personality_2_probability = 0.2 # 第二种人格出现概率 +personality_2_probability = 0.2 # 第二种人格出现概率,可以为0 personality_3_probability = 0.1 # 第三种人格出现概率,请确保三个概率相加等于1 -prompt_schedule = "用一句话或几句话描述描述性格特点和其他特征" + +[schedule] +enable_schedule_gen = true # 是否启用日程表 +prompt_schedule_gen = "用几句话描述描述性格特点或行动规律,这个特征会用来生成日程表" [message] -min_text_length = 2 # 与麦麦聊天时麦麦只会回答文本大于等于此数的消息 -max_context_size = 15 # 麦麦获得的上文数量 +max_context_size = 15 # 麦麦获得的上文数量,建议15,太短太长都会导致脑袋尖尖 emoji_chance = 0.2 # 麦麦使用表情包的概率 -thinking_timeout = 120 # 麦麦思考时间 - -response_willing_amplifier = 1 # 麦麦回复意愿放大系数,一般为1 -response_interested_rate_amplifier = 1 # 麦麦回复兴趣度放大系数,听到记忆里的内容时放大系数 -down_frequency_rate = 3 # 降低回复频率的群组回复意愿降低系数 除法 +thinking_timeout = 120 # 麦麦最长思考时间,超过这个时间的思考会放弃 +max_response_length = 1024 # 麦麦回答的最大token数 ban_words = [ # "403","张三" ] @@ -49,26 +60,25 @@ ban_msgs_regex = [ # "\\[CQ:at,qq=\\d+\\]" # 匹配@ ] -[emoji] -check_interval = 300 # 检查表情包的时间间隔 -register_interval = 20 # 注册表情包的时间间隔 -auto_save = true # 自动偷表情包 -enable_check = false # 是否启用表情包过滤 -check_prompt = "符合公序良俗" # 表情包过滤要求 - -[cq_code] -enable_pic_translate = false +[willing] +willing_mode = "classical" # 回复意愿模式 经典模式 +# willing_mode = "dynamic" # 动态模式(可能不兼容) +# willing_mode = "custom" # 自定义模式(可自行调整 +response_willing_amplifier = 1 # 麦麦回复意愿放大系数,一般为1 +response_interested_rate_amplifier = 1 # 麦麦回复兴趣度放大系数,听到记忆里的内容时放大系数 +down_frequency_rate = 3 # 降低回复频率的群组回复意愿降低系数 除法 [response] model_r1_probability = 0.8 # 麦麦回答时选择主要回复模型1 模型的概率 model_v3_probability = 0.1 # 麦麦回答时选择次要回复模型2 模型的概率 model_r1_distill_probability = 0.1 # 麦麦回答时选择次要回复模型3 模型的概率 -max_response_length = 1024 # 麦麦回答的最大token数 -[willing] -willing_mode = "classical" # 回复意愿模式 经典模式 -# willing_mode = "dynamic" # 动态模式(可能不兼容) -# willing_mode = "custom" # 自定义模式(可自行调整 +[emoji] +check_interval = 15 # 检查破损表情包的时间间隔(分钟) +register_interval = 60 # 注册表情包的时间间隔(分钟) +auto_save = true # 是否保存表情包和图片 +enable_check = false # 是否启用表情包过滤 +check_prompt = "符合公序良俗" # 表情包过滤要求 [memory] build_memory_interval = 2000 # 记忆构建间隔 单位秒 间隔越低,麦麦学习越多,但是冗余信息也会增多 @@ -81,7 +91,6 @@ forget_memory_interval = 1000 # 记忆遗忘间隔 单位秒 间隔越低, memory_forget_time = 24 #多长时间后的记忆会被遗忘 单位小时 memory_forget_percentage = 0.01 # 记忆遗忘比例 控制记忆遗忘程度 越大遗忘越多 建议保持默认 - memory_ban_words = [ #不希望记忆的词 # "403","张三" ] @@ -106,26 +115,17 @@ reaction = "回答“测试成功”" [chinese_typo] enable = true # 是否启用中文错别字生成器 -error_rate=0.002 # 单字替换概率 +error_rate=0.001 # 单字替换概率 min_freq=9 # 最小字频阈值 -tone_error_rate=0.2 # 声调错误概率 +tone_error_rate=0.1 # 声调错误概率 word_replace_rate=0.006 # 整词替换概率 -[others] -enable_kuuki_read = true # 是否启用读空气功能 -enable_friend_chat = false # 是否启用好友聊天 - -[groups] -talk_allowed = [ - 123, - 123, -] #可以回复消息的群 -talk_frequency_down = [] #降低回复频率的群 -ban_user_id = [] #禁止回复和读取消息的QQ号 - [remote] #发送统计信息,主要是看全球有多少只麦麦 enable = true +[experimental] +enable_friend_chat = false # 是否启用好友聊天 +enable_thinkflow = false # 是否启用思维流 #下面的模型若使用硅基流动则不需要更改,使用ds官方则改成.env.prod自定义的宏,使用自定义模型则选择定位相似的模型自己填写 #推理模型 @@ -188,3 +188,11 @@ pri_out = 0.35 [model.embedding] #嵌入 name = "BAAI/bge-m3" provider = "SILICONFLOW" + +#测试模型,给think_glow用,如果你没开实验性功能,随便写就行,但是要有 +[model.llm_outer_world] #外世界判断:建议使用qwen2.5 7b +# name = "Pro/Qwen/Qwen2.5-7B-Instruct" +name = "Qwen/Qwen2.5-7B-Instruct" +provider = "SILICONFLOW" +pri_in = 0 +pri_out = 0 \ No newline at end of file From 51990391fd5e6bedb189e042c299f70ba41e55d8 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Tue, 25 Mar 2025 22:27:38 +0800 Subject: [PATCH 06/46] =?UTF-8?q?better=20=E6=96=B0=E5=A2=9E=E4=BA=86?= =?UTF-8?q?=E5=88=86=E5=89=B2=E5=99=A8=EF=BC=8C=E8=A1=A8=E6=83=85=E6=83=A9?= =?UTF-8?q?=E7=BD=9A=E7=B3=BB=E6=95=B0=E7=9A=84=E8=87=AA=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/bot_config_test.toml | 179 -------------------------- src/plugins/chat/config.py | 16 ++- src/plugins/chat/utils.py | 29 +++-- src/plugins/willing/mode_classical.py | 3 +- template/bot_config_template.toml | 9 +- 5 files changed, 40 insertions(+), 196 deletions(-) delete mode 100644 config/bot_config_test.toml diff --git a/config/bot_config_test.toml b/config/bot_config_test.toml deleted file mode 100644 index dd01bfded..000000000 --- a/config/bot_config_test.toml +++ /dev/null @@ -1,179 +0,0 @@ -[inner] -version = "0.0.10" - -[mai_version] -version = "0.6.0" -version-fix = "snapshot-1" - -#以下是给开发人员阅读的,一般用户不需要阅读 -#如果你想要修改配置文件,请在修改后将version的值进行变更 -#如果新增项目,请在BotConfig类下新增相应的变量 -#1.如果你修改的是[]层级项目,例如你新增了 [memory],那么请在config.py的 load_config函数中的include_configs字典中新增"内容":{ -#"func":memory, -#"support":">=0.0.0", #新的版本号 -#"necessary":False #是否必须 -#} -#2.如果你修改的是[]下的项目,例如你新增了[memory]下的 memory_ban_words ,那么请在config.py的 load_config函数中的 memory函数下新增版本判断: - # if config.INNER_VERSION in SpecifierSet(">=0.0.2"): - # config.memory_ban_words = set(memory_config.get("memory_ban_words", [])) - -[bot] -qq = 2814567326 -nickname = "麦麦" -alias_names = ['牢麦', '麦叠', '哈基麦'] - -[personality] -prompt_personality = ['曾经是一个学习地质的女大学生,现在学习心理学和脑科学,你会刷贴吧', '是一个女大学生,你有黑色头发,你会刷小红书', '是一个女大学生,你会刷b站,对ACG文化感兴趣'] -personality_1_probability = 0.7 # 第一种人格出现概率 -personality_2_probability = 0.1 # 第二种人格出现概率 -personality_3_probability = 0.2 # 第三种人格出现概率,请确保三个概率相加等于1 -prompt_schedule = "一个曾经学习地质,现在学习心理学和脑科学的女大学生,喜欢刷qq,贴吧,知乎和小红书" - -[message] -min_text_length = 2 # 与麦麦聊天时麦麦只会回答文本大于等于此数的消息 -max_context_size = 10 # 麦麦获得的上文数量 -emoji_chance = 0.2 # 麦麦使用表情包的概率 -thinking_timeout = 100 # 麦麦思考时间 - -response_willing_amplifier = 1 # 麦麦回复意愿放大系数,一般为1 -response_interested_rate_amplifier = 1 # 麦麦回复兴趣度放大系数,听到记忆里的内容时放大系数 -down_frequency_rate = 2 # 降低回复频率的群组回复意愿降低系数 -ban_words = [] - -ban_msgs_regex = [] - -[emoji] -check_interval = 120 # 检查表情包的时间间隔 -register_interval = 10 # 注册表情包的时间间隔 -auto_save = true # 自动偷表情包 -enable_check = false # 是否启用表情包过滤 -check_prompt = "符合公序良俗" # 表情包过滤要求 - -[cq_code] -enable_pic_translate = false - -[response] -model_r1_probability = 0.5 # 麦麦回答时选择主要回复模型1 模型的概率 -model_v3_probability = 0.5 # 麦麦回答时选择次要回复模型2 模型的概率 -model_r1_distill_probability = 0 # 麦麦回答时选择次要回复模型3 模型的概率 -max_response_length = 1024 # 麦麦回答的最大token数 - -[willing] -willing_mode = "classical" # 回复意愿模式 经典模式 -# willing_mode = "dynamic" # 动态模式(可能不兼容) -# willing_mode = "custom" # 自定义模式(可自行调整 - -[memory] -build_memory_interval = 3000 # 记忆构建间隔 单位秒 间隔越低,麦麦学习越多,但是冗余信息也会增多 -build_memory_distribution = [4, 4, 0.6, 48, 36, 0.4] # 记忆构建分布,参数:分布1均值,标准差,权重,分布2均值,标准差,权重 -build_memory_sample_num = 10 # 采样数量,数值越高记忆采样次数越多 -build_memory_sample_length = 30 # 采样长度,数值越高一段记忆内容越丰富 -memory_compress_rate = 0.1 # 记忆压缩率 控制记忆精简程度 建议保持默认,调高可以获得更多信息,但是冗余信息也会增多 - -forget_memory_interval = 300 # 记忆遗忘间隔 单位秒 间隔越低,麦麦遗忘越频繁,记忆更精简,但更难学习 -memory_forget_time = 24 #多长时间后的记忆会被遗忘 单位小时 -memory_forget_percentage = 0.005 # 记忆遗忘比例 控制记忆遗忘程度 越大遗忘越多 建议保持默认 - - -memory_ban_words = ['表情包', '图片', '回复', '聊天记录'] - -[mood] -mood_update_interval = 1.0 # 情绪更新间隔 单位秒 -mood_decay_rate = 0.95 # 情绪衰减率 -mood_intensity_factor = 1.0 # 情绪强度因子 - -[keywords_reaction] # 针对某个关键词作出反应 -enable = true # 关键词反应功能的总开关 - -[[keywords_reaction.rules]] -enable = true -keywords = [ "人机", "bot", "机器", "入机", "robot", "机器人",] -reaction = "有人说你是人机或者机器人,否定这一事实,攻击或反问他。注意,只有认为你是机器人才回复,否则不要否认" - -[[keywords_reaction.rules]] -enable = false -keywords = [ "测试关键词回复", "test", "",] -reaction = "回答“测试成功”" - -[chinese_typo] -enable = true # 是否启用中文错别字生成器 -error_rate=0.01 # 单字替换概率 -min_freq=7 # 最小字频阈值 -tone_error_rate=0.3 # 声调错误概率 -word_replace_rate=0.01 # 整词替换概率 - -[others] -enable_kuuki_read = true # 是否启用读空气功能 -enable_friend_chat = true # 是否启用好友聊天 - -[groups] -talk_allowed = [571780722,1022489779,534940728, 192194125, 851345375, 739044565, 766798517, 1030993430, 435591861, 708847644, 591693379, 571780722, 1028699246, 571780722, 1015816696] #可以回复消息的群 -talk_frequency_down = [1022489779, 571780722] #降低回复频率的群 -ban_user_id = [3488737411, 2732836727, 3878664193, 3799953254] #禁止回复和读取消息的QQ号 - -[remote] #发送统计信息,主要是看全球有多少只麦麦 -enable = true - -#下面的模型若使用硅基流动则不需要更改,使用ds官方则改成.env.prod自定义的宏,使用自定义模型则选择定位相似的模型自己填写 - -#推理模型 - -[model.llm_reasoning] #回复模型1 主要回复模型 -# name = "Pro/deepseek-ai/DeepSeek-R1" -name = "Qwen/QwQ-32B" -provider = "SILICONFLOW" -pri_in = 1.0 #模型的输入价格(非必填,可以记录消耗) -pri_out = 4.0 #模型的输出价格(非必填,可以记录消耗) - -[model.llm_reasoning_minor] #回复模型3 次要回复模型 -name = "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B" -provider = "SILICONFLOW" -pri_in = 1.26 #模型的输入价格(非必填,可以记录消耗) -pri_out = 1.26 #模型的输出价格(非必填,可以记录消耗) - -#非推理模型 - -[model.llm_normal] #V3 回复模型2 次要回复模型 -name = "Qwen/Qwen2.5-32B-Instruct" -provider = "SILICONFLOW" -pri_in = 1.26 #模型的输入价格(非必填,可以记录消耗) -pri_out = 1.26 #模型的输出价格(非必填,可以记录消耗) - -[model.llm_emotion_judge] #表情包判断 -name = "Qwen/Qwen2.5-14B-Instruct" -provider = "SILICONFLOW" -pri_in = 0.7 -pri_out = 0.7 - -[model.llm_topic_judge] #记忆主题判断:建议使用qwen2.5 7b -name = "Pro/Qwen/Qwen2.5-7B-Instruct" -# name = "Qwen/Qwen2-1.5B-Instruct" -provider = "SILICONFLOW" -pri_in = 0.35 -pri_out = 0.35 - -[model.llm_summary_by_topic] #概括模型,建议使用qwen2.5 32b 及以上 -name = "Qwen/Qwen2.5-32B-Instruct" -provider = "SILICONFLOW" -pri_in = 1.26 -pri_out = 1.26 - -[model.moderation] #内容审核,开发中 -name = "" -provider = "SILICONFLOW" -pri_in = 1.0 -pri_out = 2.0 - -# 识图模型 - -[model.vlm] #图像识别 -name = "Pro/Qwen/Qwen2.5-VL-7B-Instruct" -provider = "SILICONFLOW" -pri_in = 0.35 -pri_out = 0.35 - -#嵌入模型 - -[model.embedding] #嵌入 -name = "BAAI/bge-m3" -provider = "SILICONFLOW" diff --git a/src/plugins/chat/config.py b/src/plugins/chat/config.py index b16af9137..54303b959 100644 --- a/src/plugins/chat/config.py +++ b/src/plugins/chat/config.py @@ -57,6 +57,7 @@ class BotConfig: response_willing_amplifier: float = 1.0 # 回复意愿放大系数 response_interested_rate_amplifier: float = 1.0 # 回复兴趣度放大系数 down_frequency_rate: float = 3 # 降低回复频率的群组回复意愿降低系数 + emoji_response_penalty: float = 0.0 # 表情包回复惩罚 # response MODEL_R1_PROBABILITY: float = 0.8 # R1模型概率 @@ -101,6 +102,11 @@ class BotConfig: chinese_typo_min_freq = 7 # 最小字频阈值 chinese_typo_tone_error_rate = 0.2 # 声调错误概率 chinese_typo_word_replace_rate = 0.02 # 整词替换概率 + + #response_spliter + enable_response_spliter = True # 是否启用回复分割器 + response_max_length = 100 # 回复允许的最大长度 + response_max_sentence_num = 3 # 回复允许的最大句子数 # remote remote_enable: bool = True # 是否启用远程控制 @@ -242,7 +248,8 @@ class BotConfig: config.response_willing_amplifier = willing_config.get("response_willing_amplifier", config.response_willing_amplifier) config.response_interested_rate_amplifier = willing_config.get("response_interested_rate_amplifier", config.response_interested_rate_amplifier) config.down_frequency_rate = willing_config.get("down_frequency_rate", config.down_frequency_rate) - + config.emoji_response_penalty = willing_config.get("emoji_response_penalty", config.emoji_response_penalty) + def model(parent: dict): # 加载模型配置 model_config: dict = parent["model"] @@ -378,6 +385,12 @@ class BotConfig: config.chinese_typo_word_replace_rate = chinese_typo_config.get( "word_replace_rate", config.chinese_typo_word_replace_rate ) + + def response_spliter(parent: dict): + response_spliter_config = parent["response_spliter"] + config.enable_response_spliter = response_spliter_config.get("enable_response_spliter", config.enable_response_spliter) + config.response_max_length = response_spliter_config.get("response_max_length", config.response_max_length) + config.response_max_sentence_num = response_spliter_config.get("response_max_sentence_num", config.response_max_sentence_num) def groups(parent: dict): groups_config = parent["groups"] @@ -409,6 +422,7 @@ class BotConfig: "remote": {"func": remote, "support": ">=0.0.10", "necessary": False}, "keywords_reaction": {"func": keywords_reaction, "support": ">=0.0.2", "necessary": False}, "chinese_typo": {"func": chinese_typo, "support": ">=0.0.3", "necessary": False}, + "response_spliter": {"func": response_spliter, "support": ">=0.0.11", "necessary": False}, "experimental": {"func": experimental, "support": ">=0.0.11", "necessary": False}, } diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index 0d63e7afc..ef9878c4e 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -244,21 +244,17 @@ def split_into_sentences_w_remove_punctuation(text: str) -> List[str]: List[str]: 分割后的句子列表 """ len_text = len(text) - if len_text < 5: + if len_text < 4: if random.random() < 0.01: return list(text) # 如果文本很短且触发随机条件,直接按字符分割 else: return [text] if len_text < 12: - split_strength = 0.3 + split_strength = 0.2 elif len_text < 32: - split_strength = 0.7 + split_strength = 0.6 else: - split_strength = 0.9 - # 先移除换行符 - # print(f"split_strength: {split_strength}") - - # print(f"处理前的文本: {text}") + split_strength = 0.7 # 检查是否为西文字符段落 if not is_western_paragraph(text): @@ -348,7 +344,7 @@ def random_remove_punctuation(text: str) -> str: for i, char in enumerate(text): if char == "。" and i == text_len - 1: # 结尾的句号 - if random.random() > 0.4: # 80%概率删除结尾句号 + if random.random() > 0.1: # 90%概率删除结尾句号 continue elif char == ",": rand = random.random() @@ -364,10 +360,12 @@ def random_remove_punctuation(text: str) -> str: def process_llm_response(text: str) -> List[str]: # processed_response = process_text_with_typos(content) # 对西文字符段落的回复长度设置为汉字字符的两倍 - if len(text) > 100 and not is_western_paragraph(text) : + max_length = global_config.response_max_length + max_sentence_num = global_config.response_max_sentence_num + if len(text) > max_length and not is_western_paragraph(text) : logger.warning(f"回复过长 ({len(text)} 字符),返回默认回复") return ["懒得说"] - elif len(text) > 200 : + elif len(text) > max_length * 2 : logger.warning(f"回复过长 ({len(text)} 字符),返回默认回复") return ["懒得说"] # 处理长消息 @@ -377,7 +375,10 @@ def process_llm_response(text: str) -> List[str]: tone_error_rate=global_config.chinese_typo_tone_error_rate, word_replace_rate=global_config.chinese_typo_word_replace_rate, ) - split_sentences = split_into_sentences_w_remove_punctuation(text) + if global_config.enable_response_spliter: + split_sentences = split_into_sentences_w_remove_punctuation(text) + else: + split_sentences = [text] sentences = [] for sentence in split_sentences: if global_config.chinese_typo_enable: @@ -389,14 +390,14 @@ def process_llm_response(text: str) -> List[str]: sentences.append(sentence) # 检查分割后的消息数量是否过多(超过3条) - if len(sentences) > 3: + if len(sentences) > max_sentence_num: logger.warning(f"分割后消息数量过多 ({len(sentences)} 条),返回默认回复") return [f"{global_config.BOT_NICKNAME}不知道哦"] return sentences -def calculate_typing_time(input_string: str, chinese_time: float = 0.4, english_time: float = 0.2) -> float: +def calculate_typing_time(input_string: str, chinese_time: float = 0.2, english_time: float = 0.1) -> float: """ 计算输入字符串所需的时间,中文和英文字符有不同的输入时间 input_string (str): 输入的字符串 diff --git a/src/plugins/willing/mode_classical.py b/src/plugins/willing/mode_classical.py index a131b576d..a0ec90ffc 100644 --- a/src/plugins/willing/mode_classical.py +++ b/src/plugins/willing/mode_classical.py @@ -1,6 +1,7 @@ import asyncio from typing import Dict from ..chat.chat_stream import ChatStream +from ..chat.config import global_config class WillingManager: @@ -51,7 +52,7 @@ class WillingManager: current_willing += 0.05 if is_emoji: - current_willing *= 0.2 + current_willing *= global_config.emoji_response_penalty self.chat_reply_willing[chat_id] = min(current_willing, 3.0) diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index dcd3403af..c8ce896ec 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -67,6 +67,7 @@ willing_mode = "classical" # 回复意愿模式 经典模式 response_willing_amplifier = 1 # 麦麦回复意愿放大系数,一般为1 response_interested_rate_amplifier = 1 # 麦麦回复兴趣度放大系数,听到记忆里的内容时放大系数 down_frequency_rate = 3 # 降低回复频率的群组回复意愿降低系数 除法 +emoji_response_penalty = 0.1 # 表情包回复惩罚系数,设为0为不回复单个表情包,减少单独回复表情包的概率 [response] model_r1_probability = 0.8 # 麦麦回答时选择主要回复模型1 模型的概率 @@ -105,7 +106,7 @@ enable = true # 关键词反应功能的总开关 [[keywords_reaction.rules]] # 如果想要新增多个关键词,直接复制本条,修改keywords和reaction即可 enable = true # 是否启用此条(为了人类在未来AI战争能更好地识别AI(bushi),默认开启) -keywords = ["人机", "bot", "机器", "入机", "robot", "机器人"] # 会触发反应的关键词 +keywords = ["人机", "bot", "机器", "入机", "robot", "机器人","ai","AI"] # 会触发反应的关键词 reaction = "有人说你是人机或者机器人,否定这一事实,攻击或反问他。注意,只有认为你是机器人才回复,否则不要否认" # 触发之后添加的提示词 [[keywords_reaction.rules]] # 就像这样复制 @@ -120,6 +121,12 @@ min_freq=9 # 最小字频阈值 tone_error_rate=0.1 # 声调错误概率 word_replace_rate=0.006 # 整词替换概率 +[response_spliter] +enable_response_spliter = true # 是否启用回复分割器 +response_max_length = 100 # 回复允许的最大长度 +response_max_sentence_num = 4 # 回复允许的最大句子数 + + [remote] #发送统计信息,主要是看全球有多少只麦麦 enable = true From 0ea57c4a583d16cbfd1fa79fa27b450ed28914c3 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Tue, 25 Mar 2025 22:40:43 +0800 Subject: [PATCH 07/46] =?UTF-8?q?feat=20=E5=B0=86=E5=BF=83=E6=B5=81?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E4=BD=9C=E4=B8=BA=20=E5=AE=9E=E9=AA=8C?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/__init__.py | 13 ++++++----- src/plugins/chat/bot.py | 35 ++++++++++++++++++----------- src/plugins/chat/config.py | 4 ++++ src/plugins/chat/prompt_builder.py | 5 ++++- src/plugins/moods/moods.py | 2 +- src/think_flow_demo/current_mind.py | 2 +- src/think_flow_demo/heartflow.py | 2 +- template/bot_config_template.toml | 18 +++++++++++++-- 8 files changed, 56 insertions(+), 25 deletions(-) diff --git a/src/plugins/chat/__init__.py b/src/plugins/chat/__init__.py index 713f1d375..d8a41fe87 100644 --- a/src/plugins/chat/__init__.py +++ b/src/plugins/chat/__init__.py @@ -51,7 +51,10 @@ async def start_think_flow(): try: outer_world_task = asyncio.create_task(outer_world.open_eyes()) logger.success("大脑和外部世界启动成功") - return outer_world_task + # 启动心流系统 + heartflow_task = asyncio.create_task(subheartflow_manager.heartflow_start_working()) + logger.success("心流系统启动成功") + return outer_world_task, heartflow_task except Exception as e: logger.error(f"启动大脑和外部世界失败: {e}") raise @@ -70,11 +73,9 @@ async def start_background_tasks(): logger.success("情绪管理器启动成功") # 启动大脑和外部世界 - await start_think_flow() - - # 启动心流系统 - heartflow_task = asyncio.create_task(subheartflow_manager.heartflow_start_working()) - logger.success("心流系统启动成功") + if global_config.enable_think_flow: + logger.success("启动测试功能:心流系统") + await start_think_flow() # 只启动表情包管理任务 asyncio.create_task(emoji_manager.start_periodic_check()) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index a9e76648a..e89375217 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -91,9 +91,11 @@ class ChatBot: ) message.update_chat_stream(chat) + #创建 心流 观察 - await outer_world.check_and_add_new_observe() - subheartflow_manager.create_subheartflow(chat.stream_id) + if global_config.enable_think_flow: + await outer_world.check_and_add_new_observe() + subheartflow_manager.create_subheartflow(chat.stream_id) await relationship_manager.update_relationship( @@ -142,10 +144,14 @@ class ChatBot: interested_rate=interested_rate, sender_id=str(message.message_info.user_info.user_id), ) - current_willing_old = willing_manager.get_willing(chat_stream=chat) - current_willing_new = (subheartflow_manager.get_subheartflow(chat.stream_id).current_state.willing-5)/4 - print(f"旧回复意愿:{current_willing_old},新回复意愿:{current_willing_new}") - current_willing = (current_willing_old + current_willing_new) / 2 + + if global_config.enable_think_flow: + current_willing_old = willing_manager.get_willing(chat_stream=chat) + current_willing_new = (subheartflow_manager.get_subheartflow(chat.stream_id).current_state.willing-5)/4 + print(f"旧回复意愿:{current_willing_old},新回复意愿:{current_willing_new}") + current_willing = (current_willing_old + current_willing_new) / 2 + else: + current_willing = willing_manager.get_willing(chat_stream=chat) logger.info( f"[{current_time}][{chat.group_info.group_name if chat.group_info else '私聊'}]" @@ -185,13 +191,16 @@ class ChatBot: # print(f"response: {response}") if response: stream_id = message.chat_stream.stream_id - chat_talking_prompt = "" - if stream_id: - chat_talking_prompt = get_recent_group_detailed_plain_text( - stream_id, limit=global_config.MAX_CONTEXT_SIZE, combine=True - ) - - await subheartflow_manager.get_subheartflow(stream_id).do_after_reply(response,chat_talking_prompt) + + if global_config.enable_think_flow: + chat_talking_prompt = "" + if stream_id: + chat_talking_prompt = get_recent_group_detailed_plain_text( + stream_id, limit=global_config.MAX_CONTEXT_SIZE, combine=True + ) + await subheartflow_manager.get_subheartflow(stream_id).do_after_reply(response,chat_talking_prompt) + + # print(f"有response: {response}") container = message_manager.get_container(chat.stream_id) thinking_message = None diff --git a/src/plugins/chat/config.py b/src/plugins/chat/config.py index 54303b959..503ba0dcb 100644 --- a/src/plugins/chat/config.py +++ b/src/plugins/chat/config.py @@ -130,6 +130,8 @@ class BotConfig: # 实验性 llm_outer_world: Dict[str, str] = field(default_factory=lambda: {}) + llm_sub_heartflow: Dict[str, str] = field(default_factory=lambda: {}) + llm_heartflow: Dict[str, str] = field(default_factory=lambda: {}) @staticmethod @@ -265,6 +267,8 @@ class BotConfig: "embedding", "moderation", "llm_outer_world", + "llm_sub_heartflow", + "llm_heartflow", ] for item in config_list: diff --git a/src/plugins/chat/prompt_builder.py b/src/plugins/chat/prompt_builder.py index b03e6b044..e6bdaf979 100644 --- a/src/plugins/chat/prompt_builder.py +++ b/src/plugins/chat/prompt_builder.py @@ -37,7 +37,10 @@ class PromptBuilder: ) # outer_world_info = outer_world.outer_world_info - current_mind_info = subheartflow_manager.get_subheartflow(stream_id).current_mind + if global_config.enable_think_flow: + current_mind_info = subheartflow_manager.get_subheartflow(stream_id).current_mind + else: + current_mind_info = "" relation_prompt = "" for person in who_chat_in_group: diff --git a/src/plugins/moods/moods.py b/src/plugins/moods/moods.py index b09e58168..3e977d024 100644 --- a/src/plugins/moods/moods.py +++ b/src/plugins/moods/moods.py @@ -122,7 +122,7 @@ class MoodManager: time_diff = current_time - self.last_update # Valence 向中性(0)回归 - valence_target = -0.2 + valence_target = 0 self.current_mood.valence = valence_target + (self.current_mood.valence - valence_target) * math.exp( -self.decay_rate_valence * time_diff ) diff --git a/src/think_flow_demo/current_mind.py b/src/think_flow_demo/current_mind.py index 09634cf2d..2446d66d6 100644 --- a/src/think_flow_demo/current_mind.py +++ b/src/think_flow_demo/current_mind.py @@ -21,7 +21,7 @@ class SubHeartflow: self.current_mind = "" self.past_mind = [] self.current_state : CuttentState = CuttentState() - self.llm_model = LLM_request(model=global_config.llm_topic_judge, temperature=0.7, max_tokens=600, request_type="sub_heart_flow") + self.llm_model = LLM_request(model=global_config.llm_sub_heartflow, temperature=0.7, max_tokens=600, request_type="sub_heart_flow") self.outer_world = None self.main_heartflow_info = "" diff --git a/src/think_flow_demo/heartflow.py b/src/think_flow_demo/heartflow.py index 696641cb7..e455e1977 100644 --- a/src/think_flow_demo/heartflow.py +++ b/src/think_flow_demo/heartflow.py @@ -21,7 +21,7 @@ class Heartflow: self.current_mind = "你什么也没想" self.past_mind = [] self.current_state : CuttentState = CuttentState() - self.llm_model = LLM_request(model=global_config.llm_topic_judge, temperature=0.6, max_tokens=1000, request_type="heart_flow") + self.llm_model = LLM_request(model=global_config.llm_heartflow, temperature=0.6, max_tokens=1000, request_type="heart_flow") self._subheartflows = {} self.active_subheartflows_nums = 0 diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index c8ce896ec..2359b678d 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -132,7 +132,7 @@ enable = true [experimental] enable_friend_chat = false # 是否启用好友聊天 -enable_thinkflow = false # 是否启用思维流 +enable_think_flow = false # 是否启用思维流 #下面的模型若使用硅基流动则不需要更改,使用ds官方则改成.env.prod自定义的宏,使用自定义模型则选择定位相似的模型自己填写 #推理模型 @@ -202,4 +202,18 @@ provider = "SILICONFLOW" name = "Qwen/Qwen2.5-7B-Instruct" provider = "SILICONFLOW" pri_in = 0 -pri_out = 0 \ No newline at end of file +pri_out = 0 + +[model.llm_sub_heartflow] #心流:建议使用qwen2.5 7b +# name = "Pro/Qwen/Qwen2.5-7B-Instruct" +name = "Qwen/Qwen2.5-32B-Instruct" +provider = "SILICONFLOW" +pri_in = 1.26 +pri_out = 1.26 + +[model.llm_heartflow] #心流:建议使用qwen2.5 32b +# name = "Pro/Qwen/Qwen2.5-7B-Instruct" +name = "Qwen/Qwen2.5-32B-Instruct" +provider = "SILICONFLOW" +pri_in = 1.26 +pri_out = 1.26 \ No newline at end of file From 83ee182bfe36a15c145ddcef1c8a640b1b4425f1 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Tue, 25 Mar 2025 22:56:39 +0800 Subject: [PATCH 08/46] fix ruff --- src/plugins/chat/config.py | 21 ++++++++++++++------- src/plugins/chat/prompt_builder.py | 7 +++---- src/plugins/chat/relationship_manager.py | 5 +++-- src/think_flow_demo/current_mind.py | 22 ++++++++++++---------- src/think_flow_demo/heartflow.py | 10 ++++++---- src/think_flow_demo/outer_world.py | 12 +++++++----- template/bot_config_template.toml | 2 +- 7 files changed, 46 insertions(+), 33 deletions(-) diff --git a/src/plugins/chat/config.py b/src/plugins/chat/config.py index 503ba0dcb..2b73996f3 100644 --- a/src/plugins/chat/config.py +++ b/src/plugins/chat/config.py @@ -213,7 +213,8 @@ class BotConfig: schedule_config = parent["schedule"] config.ENABLE_SCHEDULE_GEN = schedule_config.get("enable_schedule_gen", config.ENABLE_SCHEDULE_GEN) config.PROMPT_SCHEDULE_GEN = schedule_config.get("prompt_schedule_gen", config.PROMPT_SCHEDULE_GEN) - logger.info(f"载入自定义日程prompt:{schedule_config.get('prompt_schedule_gen', config.PROMPT_SCHEDULE_GEN)}") + logger.info( + f"载入自定义日程prompt:{schedule_config.get('prompt_schedule_gen', config.PROMPT_SCHEDULE_GEN)}") def emoji(parent: dict): emoji_config = parent["emoji"] @@ -247,10 +248,13 @@ class BotConfig: config.willing_mode = willing_config.get("willing_mode", config.willing_mode) if config.INNER_VERSION in SpecifierSet(">=0.0.11"): - config.response_willing_amplifier = willing_config.get("response_willing_amplifier", config.response_willing_amplifier) - config.response_interested_rate_amplifier = willing_config.get("response_interested_rate_amplifier", config.response_interested_rate_amplifier) + config.response_willing_amplifier = willing_config.get( + "response_willing_amplifier", config.response_willing_amplifier) + config.response_interested_rate_amplifier = willing_config.get( + "response_interested_rate_amplifier", config.response_interested_rate_amplifier) config.down_frequency_rate = willing_config.get("down_frequency_rate", config.down_frequency_rate) - config.emoji_response_penalty = willing_config.get("emoji_response_penalty", config.emoji_response_penalty) + config.emoji_response_penalty = willing_config.get( + "emoji_response_penalty", config.emoji_response_penalty) def model(parent: dict): # 加载模型配置 @@ -392,9 +396,11 @@ class BotConfig: def response_spliter(parent: dict): response_spliter_config = parent["response_spliter"] - config.enable_response_spliter = response_spliter_config.get("enable_response_spliter", config.enable_response_spliter) + config.enable_response_spliter = response_spliter_config.get( + "enable_response_spliter", config.enable_response_spliter) config.response_max_length = response_spliter_config.get("response_max_length", config.response_max_length) - config.response_max_sentence_num = response_spliter_config.get("response_max_sentence_num", config.response_max_sentence_num) + config.response_max_sentence_num = response_spliter_config.get( + "response_max_sentence_num", config.response_max_sentence_num) def groups(parent: dict): groups_config = parent["groups"] @@ -405,7 +411,8 @@ class BotConfig: def experimental(parent: dict): experimental_config = parent["experimental"] config.enable_friend_chat = experimental_config.get("enable_friend_chat", config.enable_friend_chat) - + config.enable_think_flow = experimental_config.get("enable_think_flow", config.enable_think_flow) + # 版本表达式:>=1.0.0,<2.0.0 # 允许字段:func: method, support: str, notice: str, necessary: bool # 如果使用 notice 字段,在该组配置加载时,会展示该字段对用户的警示 diff --git a/src/plugins/chat/prompt_builder.py b/src/plugins/chat/prompt_builder.py index e6bdaf979..639e9dc08 100644 --- a/src/plugins/chat/prompt_builder.py +++ b/src/plugins/chat/prompt_builder.py @@ -13,7 +13,6 @@ from .relationship_manager import relationship_manager from src.common.logger import get_module_logger from src.think_flow_demo.heartflow import subheartflow_manager -from src.think_flow_demo.outer_world import outer_world logger = get_module_logger("prompt") @@ -58,9 +57,9 @@ class PromptBuilder: mood_prompt = mood_manager.get_prompt() # 日程构建 - current_date = time.strftime("%Y-%m-%d", time.localtime()) - current_time = time.strftime("%H:%M:%S", time.localtime()) - bot_schedule_now_time, bot_schedule_now_activity = bot_schedule.get_current_task() + # current_date = time.strftime("%Y-%m-%d", time.localtime()) + # current_time = time.strftime("%H:%M:%S", time.localtime()) + # bot_schedule_now_time, bot_schedule_now_activity = bot_schedule.get_current_task() # 获取聊天上下文 chat_in_group = True diff --git a/src/plugins/chat/relationship_manager.py b/src/plugins/chat/relationship_manager.py index 53cb0abbf..f4cda0662 100644 --- a/src/plugins/chat/relationship_manager.py +++ b/src/plugins/chat/relationship_manager.py @@ -122,11 +122,12 @@ class RelationshipManager: relationship.relationship_value = float(relationship.relationship_value.to_decimal()) else: relationship.relationship_value = float(relationship.relationship_value) - logger.info(f"[关系管理] 用户 {user_id}({platform}) 的关系值已转换为double类型: {relationship.relationship_value}") + logger.info( + f"[关系管理] 用户 {user_id}({platform}) 的关系值已转换为double类型: {relationship.relationship_value}") # noqa: E501 except (ValueError, TypeError): # 如果不能解析/强转则将relationship.relationship_value设置为double类型的0 relationship.relationship_value = 0.0 - logger.warning(f"[关系管理] 用户 {user_id}({platform}) 的关系值无法转换为double类型,已设置为0") + logger.warning(f"[关系管理] 用户 {user_id}({platform}) 的无法转换为double类型,已设置为0") relationship.relationship_value += value await self.storage_relationship(relationship) relationship.saved = True diff --git a/src/think_flow_demo/current_mind.py b/src/think_flow_demo/current_mind.py index 2446d66d6..78447215f 100644 --- a/src/think_flow_demo/current_mind.py +++ b/src/think_flow_demo/current_mind.py @@ -21,7 +21,8 @@ class SubHeartflow: self.current_mind = "" self.past_mind = [] self.current_state : CuttentState = CuttentState() - self.llm_model = LLM_request(model=global_config.llm_sub_heartflow, temperature=0.7, max_tokens=600, request_type="sub_heart_flow") + self.llm_model = LLM_request( + model=global_config.llm_sub_heartflow, temperature=0.7, max_tokens=600, request_type="sub_heart_flow") self.outer_world = None self.main_heartflow_info = "" @@ -52,15 +53,15 @@ class SubHeartflow: related_memory_info = 'memory' message_stream_info = self.outer_world.talking_summary - prompt = f"" + prompt = "" # prompt += f"麦麦的总体想法是:{self.main_heartflow_info}\n\n" prompt += f"{personality_info}\n" prompt += f"现在你正在上网,和qq群里的网友们聊天,群里正在聊的话题是:{message_stream_info}\n" prompt += f"你想起来{related_memory_info}。" prompt += f"刚刚你的想法是{current_thinking_info}。" prompt += f"你现在{mood_info}。" - prompt += f"现在你接下去继续思考,产生新的想法,不要分点输出,输出连贯的内心独白,不要太长,但是记得结合上述的消息,要记得维持住你的人设,关注聊天和新内容,不要思考太多:" - + prompt += "现在你接下去继续思考,产生新的想法,不要分点输出,输出连贯的内心独白,不要太长," + prompt += "但是记得结合上述的消息,要记得维持住你的人设,关注聊天和新内容,不要思考太多:" reponse, reasoning_content = await self.llm_model.generate_response_async(prompt) self.update_current_mind(reponse) @@ -80,7 +81,7 @@ class SubHeartflow: message_new_info = chat_talking_prompt reply_info = reply_content - prompt = f"" + prompt = "" prompt += f"{personality_info}\n" prompt += f"现在你正在上网,和qq群里的网友们聊天,群里正在聊的话题是:{message_stream_info}\n" prompt += f"你想起来{related_memory_info}。" @@ -88,7 +89,8 @@ class SubHeartflow: prompt += f"你现在看到了网友们发的新消息:{message_new_info}\n" prompt += f"你刚刚回复了群友们:{reply_info}" prompt += f"你现在{mood_info}。" - prompt += f"现在你接下去继续思考,产生新的想法,记得保留你刚刚的想法,不要分点输出,输出连贯的内心独白,不要太长,但是记得结合上述的消息,要记得你的人设,关注聊天和新内容,以及你回复的内容,不要思考太多:" + prompt += "现在你接下去继续思考,产生新的想法,记得保留你刚刚的想法,不要分点输出,输出连贯的内心独白" + prompt += "不要太长,但是记得结合上述的消息,要记得你的人设,关注聊天和新内容,以及你回复的内容,不要思考太多:" reponse, reasoning_content = await self.llm_model.generate_response_async(prompt) @@ -103,13 +105,13 @@ class SubHeartflow: current_thinking_info = self.current_mind mood_info = self.current_state.mood # print("麦麦闹情绪了2") - prompt = f"" + prompt = "" prompt += f"{personality_info}\n" - prompt += f"现在你正在上网,和qq群里的网友们聊天" + prompt += "现在你正在上网,和qq群里的网友们聊天" prompt += f"你现在的想法是{current_thinking_info}。" prompt += f"你现在{mood_info}。" - prompt += f"现在请你思考,你想不想发言或者回复,请你输出一个数字,1-10,1表示非常不想,10表示非常想。" - prompt += f"请你用<>包裹你的回复意愿,例如输出<1>表示不想回复,输出<10>表示非常想回复。请你考虑,你完全可以不回复" + prompt += "现在请你思考,你想不想发言或者回复,请你输出一个数字,1-10,1表示非常不想,10表示非常想。" + prompt += "请你用<>包裹你的回复意愿,输出<1>表示不想回复,输出<10>表示非常想回复。请你考虑,你完全可以不回复" response, reasoning_content = await self.llm_model.generate_response_async(prompt) # 解析willing值 diff --git a/src/think_flow_demo/heartflow.py b/src/think_flow_demo/heartflow.py index e455e1977..c2e32d602 100644 --- a/src/think_flow_demo/heartflow.py +++ b/src/think_flow_demo/heartflow.py @@ -2,7 +2,6 @@ from .current_mind import SubHeartflow from src.plugins.moods.moods import MoodManager from src.plugins.models.utils_model import LLM_request from src.plugins.chat.config import global_config -from .outer_world import outer_world import asyncio class CuttentState: @@ -21,7 +20,8 @@ class Heartflow: self.current_mind = "你什么也没想" self.past_mind = [] self.current_state : CuttentState = CuttentState() - self.llm_model = LLM_request(model=global_config.llm_heartflow, temperature=0.6, max_tokens=1000, request_type="heart_flow") + self.llm_model = LLM_request( + model=global_config.llm_heartflow, temperature=0.6, max_tokens=1000, request_type="heart_flow") self._subheartflows = {} self.active_subheartflows_nums = 0 @@ -50,7 +50,8 @@ class Heartflow: prompt += f"刚刚你的主要想法是{current_thinking_info}。" prompt += f"你还有一些小想法,因为你在参加不同的群聊天,是你正在做的事情:{sub_flows_info}\n" prompt += f"你现在{mood_info}。" - prompt += f"现在你接下去继续思考,产生新的想法,但是要基于原有的主要想法,不要分点输出,输出连贯的内心独白,不要太长,但是记得结合上述的消息,关注新内容:" + prompt += "现在你接下去继续思考,产生新的想法,但是要基于原有的主要想法,不要分点输出," + prompt += "输出连贯的内心独白,不要太长,但是记得结合上述的消息,关注新内容:" reponse, reasoning_content = await self.llm_model.generate_response_async(prompt) @@ -84,7 +85,8 @@ class Heartflow: prompt += f"现在麦麦的想法是:{self.current_mind}\n" prompt += f"现在麦麦在qq群里进行聊天,聊天的话题如下:{minds_str}\n" prompt += f"你现在{mood_info}\n" - prompt += f"现在请你总结这些聊天内容,注意关注聊天内容对原有的想法的影响,输出连贯的内心独白,不要太长,但是记得结合上述的消息,要记得你的人设,关注新内容:" + prompt += '''现在请你总结这些聊天内容,注意关注聊天内容对原有的想法的影响,输出连贯的内心独白 + 不要太长,但是记得结合上述的消息,要记得你的人设,关注新内容:''' reponse, reasoning_content = await self.llm_model.generate_response_async(prompt) diff --git a/src/think_flow_demo/outer_world.py b/src/think_flow_demo/outer_world.py index 58eb4bbed..c56456bb0 100644 --- a/src/think_flow_demo/outer_world.py +++ b/src/think_flow_demo/outer_world.py @@ -3,7 +3,6 @@ import asyncio from datetime import datetime from src.plugins.models.utils_model import LLM_request from src.plugins.chat.config import global_config -import sys from src.common.database import db #存储一段聊天的大致内容 @@ -19,7 +18,8 @@ class Talking_info: self.oberve_interval = 3 - self.llm_summary = LLM_request(model=global_config.llm_outer_world, temperature=0.7, max_tokens=300, request_type="outer_world") + self.llm_summary = LLM_request( + model=global_config.llm_outer_world, temperature=0.7, max_tokens=300, request_type="outer_world") async def start_observe(self): while True: @@ -73,8 +73,9 @@ class Talking_info: prompt = "" prompt = f"你正在参与一个qq群聊的讨论,这个群之前在聊的内容是:{self.talking_summary}\n" prompt += f"现在群里的群友们产生了新的讨论,有了新的发言,具体内容如下:{self.talking_message_str}\n" - prompt += f"以上是群里在进行的聊天,请你对这个聊天内容进行总结,总结内容要包含聊天的大致内容,以及聊天中的一些重要信息,记得不要分点,不要太长,精简的概括成一段文本\n" - prompt += f"总结概括:" + prompt += '''以上是群里在进行的聊天,请你对这个聊天内容进行总结,总结内容要包含聊天的大致内容, + 以及聊天中的一些重要信息,记得不要分点,不要太长,精简的概括成一段文本\n''' + prompt += "总结概括:" self.talking_summary, reasoning_content = await self.llm_summary.generate_response_async(prompt) def translate_message_list_to_str(self): @@ -94,7 +95,8 @@ class OuterWorld: self.outer_world_info = "" self.start_time = int(datetime.now().timestamp()) - self.llm_summary = LLM_request(model=global_config.llm_topic_judge, temperature=0.7, max_tokens=600, request_type="outer_world_info") + self.llm_summary = LLM_request( + model=global_config.llm_outer_world, temperature=0.7, max_tokens=600, request_type="outer_world_info") async def check_and_add_new_observe(self): # 获取所有聊天流 diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 2359b678d..e025df46c 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -41,7 +41,7 @@ personality_2_probability = 0.2 # 第二种人格出现概率,可以为0 personality_3_probability = 0.1 # 第三种人格出现概率,请确保三个概率相加等于1 [schedule] -enable_schedule_gen = true # 是否启用日程表 +enable_schedule_gen = true # 是否启用日程表(尚未完成) prompt_schedule_gen = "用几句话描述描述性格特点或行动规律,这个特征会用来生成日程表" [message] From 6071317aca77207aa3a2739085188d79cefb836b Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Tue, 25 Mar 2025 23:06:14 +0800 Subject: [PATCH 09/46] Update prompt_builder.py --- src/plugins/chat/prompt_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/chat/prompt_builder.py b/src/plugins/chat/prompt_builder.py index 639e9dc08..73f0b0b84 100644 --- a/src/plugins/chat/prompt_builder.py +++ b/src/plugins/chat/prompt_builder.py @@ -166,7 +166,7 @@ class PromptBuilder: 你的网名叫{global_config.BOT_NICKNAME},有人也叫你{"/".join(global_config.BOT_ALIAS_NAMES)},{prompt_personality}。 你正在{chat_target_2},现在请你读读之前的聊天记录,然后给出日常且口语化的回复,平淡一些, 尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,不要回复的太有条理,可以有个性。{prompt_ger} -请回复的平淡一些,简短一些,不要刻意突出自身学科背景, +请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景, 请注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。 {moderation_prompt}不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或@等)。""" From f71ce11524d9dd00dbe28ffc9a6b7c9ab6ad39a1 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Tue, 25 Mar 2025 23:11:30 +0800 Subject: [PATCH 10/46] Update prompt_builder.py --- src/plugins/chat/prompt_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/chat/prompt_builder.py b/src/plugins/chat/prompt_builder.py index 73f0b0b84..ef070ed24 100644 --- a/src/plugins/chat/prompt_builder.py +++ b/src/plugins/chat/prompt_builder.py @@ -168,7 +168,7 @@ class PromptBuilder: 尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,不要回复的太有条理,可以有个性。{prompt_ger} 请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景, 请注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。 -{moderation_prompt}不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或@等)。""" +{moderation_prompt}不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""" prompt_check_if_response = "" From 792d65ec1ccc5b7ed93354ddd847cd3c6223fcb8 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Tue, 25 Mar 2025 23:47:52 +0800 Subject: [PATCH 11/46] =?UTF-8?q?update=20=E6=9B=B4=E6=96=B0=E6=97=A5?= =?UTF-8?q?=E5=BF=9700.6.0-snapshot-1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- changelog.md | 95 ++++++++++++++++++++++++++++++++++++ changelog_config.md | 28 +++++++++-- src/plugins/chat/__init__.py | 5 +- src/plugins/chat/config.py | 7 +++ 4 files changed, 129 insertions(+), 6 deletions(-) diff --git a/changelog.md b/changelog.md index 6841720b8..6c6b21280 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,100 @@ # Changelog AI总结 +## [0.6.0] - 2025-3-25 +### 🌟 核心功能增强 +#### 思维流系统(实验性功能) +- 新增思维流作为实验功能 +- 思维流大核+小核架构 +- 思维流回复意愿模式 + +#### 记忆系统优化 +- 优化记忆抽取策略 +- 优化记忆prompt结构 + +#### 关系系统优化 +- 修复relationship_value类型错误 +- 优化关系管理系统 +- 改进关系值计算方式 + +### 💻 系统架构优化 +#### 配置系统改进 +- 优化配置文件整理 +- 新增分割器功能 +- 新增表情惩罚系数自定义 +- 修复配置文件保存问题 +- 优化配置项管理 +- 新增配置项: + - `schedule`: 日程表生成功能配置 + - `response_spliter`: 回复分割控制 + - `experimental`: 实验性功能开关 + - `llm_outer_world`和`llm_sub_heartflow`: 思维流模型配置 + - `llm_heartflow`: 思维流核心模型配置 + - `prompt_schedule_gen`: 日程生成提示词配置 + - `memory_ban_words`: 记忆过滤词配置 +- 优化配置结构: + - 调整模型配置组织结构 + - 优化配置项默认值 + - 调整配置项顺序 +- 移除冗余配置 + +#### WebUI改进 +- 新增回复意愿模式选择功能 +- 优化WebUI界面 +- 优化WebUI配置保存机制 + +#### 部署支持扩展 +- 优化Docker构建流程 +- 完善Windows脚本支持 +- 优化Linux一键安装脚本 +- 新增macOS教程支持 + +### 🐛 问题修复 +#### 功能稳定性 +- 修复表情包审查器问题 +- 修复心跳发送问题 +- 修复拍一拍消息处理异常 +- 修复日程报错问题 +- 修复文件读写编码问题 +- 修复西文字符分割问题 +- 修复自定义API提供商识别问题 +- 修复人格设置保存问题 +- 修复EULA和隐私政策编码问题 +- 修复cfg变量引用问题 + +#### 性能优化 +- 提高topic提取效率 +- 优化logger输出格式 +- 优化cmd清理功能 +- 改进LLM使用统计 +- 优化记忆处理效率 + +### 📚 文档更新 +- 更新README.md内容 +- 添加macOS部署教程 +- 优化文档结构 +- 更新EULA和隐私政策 +- 完善部署文档 + +### 🔧 其他改进 +- 新增神秘小测验功能 +- 新增人格测评模型 +- 优化表情包审查功能 +- 改进消息转发处理 +- 优化代码风格和格式 +- 完善异常处理机制 +- 优化日志输出格式 + +### 主要改进方向 +1. 完善思维流系统功能 +2. 优化记忆系统效率 +3. 改进关系系统稳定性 +4. 提升配置系统可用性 +5. 加强WebUI功能 +6. 完善部署文档 + + + ## [0.5.15] - 2025-3-17 ### 🌟 核心功能增强 #### 关系系统升级 @@ -213,3 +307,4 @@ AI总结 + diff --git a/changelog_config.md b/changelog_config.md index c4c560644..92a522a2e 100644 --- a/changelog_config.md +++ b/changelog_config.md @@ -1,12 +1,32 @@ # Changelog +## [0.0.11] - 2025-3-12 +### Added +- 新增了 `schedule` 配置项,用于配置日程表生成功能 +- 新增了 `response_spliter` 配置项,用于控制回复分割 +- 新增了 `experimental` 配置项,用于实验性功能开关 +- 新增了 `llm_outer_world` 和 `llm_sub_heartflow` 模型配置 +- 新增了 `llm_heartflow` 模型配置 +- 在 `personality` 配置项中新增了 `prompt_schedule_gen` 参数 + +### Changed +- 优化了模型配置的组织结构 +- 调整了部分配置项的默认值 +- 调整了配置项的顺序,将 `groups` 配置项移到了更靠前的位置 +- 在 `message` 配置项中: + - 新增了 `max_response_length` 参数 +- 在 `willing` 配置项中新增了 `emoji_response_penalty` 参数 +- 将 `personality` 配置项中的 `prompt_schedule` 重命名为 `prompt_schedule_gen` + +### Removed +- 移除了 `min_text_length` 配置项 +- 移除了 `cq_code` 配置项 +- 移除了 `others` 配置项(其功能已整合到 `experimental` 中) + ## [0.0.5] - 2025-3-11 ### Added - 新增了 `alias_names` 配置项,用于指定麦麦的别名。 ## [0.0.4] - 2025-3-9 ### Added -- 新增了 `memory_ban_words` 配置项,用于指定不希望记忆的词汇。 - - - +- 新增了 `memory_ban_words` 配置项,用于指定不希望记忆的词汇。 \ No newline at end of file diff --git a/src/plugins/chat/__init__.py b/src/plugins/chat/__init__.py index d8a41fe87..39f3ddfbd 100644 --- a/src/plugins/chat/__init__.py +++ b/src/plugins/chat/__init__.py @@ -36,8 +36,9 @@ config = driver.config # 初始化表情管理器 emoji_manager.initialize() - -logger.debug(f"正在唤醒{global_config.BOT_NICKNAME}......") +logger.success("--------------------------------") +logger.success(f"正在唤醒{global_config.BOT_NICKNAME}......使用版本:{global_config.MAI_VERSION}") +logger.success("--------------------------------") # 注册消息处理器 msg_in = on_message(priority=5) # 注册和bot相关的通知处理器 diff --git a/src/plugins/chat/config.py b/src/plugins/chat/config.py index 2b73996f3..2d9badbc0 100644 --- a/src/plugins/chat/config.py +++ b/src/plugins/chat/config.py @@ -196,6 +196,12 @@ class BotConfig: def load_config(cls, config_path: str = None) -> "BotConfig": """从TOML配置文件加载配置""" config = cls() + + def mai_version(parent: dict): + mai_version_config = parent["mai_version"] + version = mai_version_config.get("version") + version_fix = mai_version_config.get("version-fix") + config.MAI_VERSION = f"{version}-{version_fix}" def personality(parent: dict): personality_config = parent["personality"] @@ -420,6 +426,7 @@ class BotConfig: # 正常执行程序,但是会看到这条自定义提示 include_configs = { "bot": {"func": bot, "support": ">=0.0.0"}, + "mai_version": {"func": mai_version, "support": ">=0.0.11"}, "groups": {"func": groups, "support": ">=0.0.0"}, "personality": {"func": personality, "support": ">=0.0.0"}, "schedule": {"func": schedule, "support": ">=0.0.11", "necessary": False}, From 1b960b32b4c87ad5886ed1dbcdd8182d1a73b242 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 26 Mar 2025 00:03:33 +0800 Subject: [PATCH 12/46] Update __init__.py --- src/plugins/chat/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plugins/chat/__init__.py b/src/plugins/chat/__init__.py index 39f3ddfbd..78e026ca7 100644 --- a/src/plugins/chat/__init__.py +++ b/src/plugins/chat/__init__.py @@ -18,7 +18,6 @@ from ..memory_system.memory import hippocampus from .message_sender import message_manager, message_sender from .storage import MessageStorage from src.common.logger import get_module_logger -# from src.think_flow_demo.current_mind import subheartflow from src.think_flow_demo.outer_world import outer_world from src.think_flow_demo.heartflow import subheartflow_manager From 681e1aa0fcd72431c6b3ed04397002ea3456b63b Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 26 Mar 2025 00:16:56 +0800 Subject: [PATCH 13/46] Merge remote-tracking branch 'origin/main-fix' into think_flow_test --- MaiLauncher.bat | 14 ++ bot.py | 2 - docs/doc1.md | 203 ++++++++++++++++++-------- docs/docker_deploy.md | 2 +- src/plugins/chat/llm_generator.py | 2 +- src/plugins/willing/mode_classical.py | 3 +- 6 files changed, 160 insertions(+), 66 deletions(-) diff --git a/MaiLauncher.bat b/MaiLauncher.bat index 619f9c65d..03e59b590 100644 --- a/MaiLauncher.bat +++ b/MaiLauncher.bat @@ -277,6 +277,19 @@ if defined VIRTUAL_ENV ( goto menu ) +if exist "%_root%\config\conda_env" ( + set /p CONDA_ENV=<"%_root%\config\conda_env" + call conda activate !CONDA_ENV! || ( + echo 激活失败,可能原因: + echo 1. 环境不存在 + echo 2. conda配置异常 + pause + goto conda_menu + ) + echo 成功激活conda环境:!CONDA_ENV! + goto menu +) + echo ===================================== echo 虚拟环境检测警告: echo 当前使用系统Python路径:!PYTHON_HOME! @@ -390,6 +403,7 @@ call conda activate !CONDA_ENV! || ( goto conda_menu ) echo 成功激活conda环境:!CONDA_ENV! +echo !CONDA_ENV! > "%_root%\config\conda_env" echo 要安装依赖吗? set /p install_confirm="继续?(Y/N): " if /i "!install_confirm!"=="Y" ( diff --git a/bot.py b/bot.py index 4f649ed92..30714e846 100644 --- a/bot.py +++ b/bot.py @@ -139,12 +139,10 @@ async def graceful_shutdown(): uvicorn_server.force_exit = True # 强制退出 await uvicorn_server.shutdown() - logger.info("正在关闭所有任务...") tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] for task in tasks: task.cancel() await asyncio.gather(*tasks, return_exceptions=True) - logger.info("所有任务已关闭") except Exception as e: logger.error(f"麦麦关闭失败: {e}") diff --git a/docs/doc1.md b/docs/doc1.md index 79ef7812e..e8aa0f0d6 100644 --- a/docs/doc1.md +++ b/docs/doc1.md @@ -5,88 +5,171 @@ - **README.md**: 项目的概述和使用说明。 - **requirements.txt**: 项目所需的Python依赖包列表。 - **bot.py**: 主启动文件,负责环境配置加载和NoneBot初始化。 -- **webui.py**: Web界面实现,提供图形化操作界面。 - **template.env**: 环境变量模板文件。 - **pyproject.toml**: Python项目配置文件。 - **docker-compose.yml** 和 **Dockerfile**: Docker配置文件,用于容器化部署。 -- **run_*.bat**: 各种启动脚本,包括开发环境、WebUI和记忆可视化等功能。 -- **EULA.md** 和 **PRIVACY.md**: 用户协议和隐私政策文件。 -- **changelog.md**: 版本更新日志。 +- **run_*.bat**: 各种启动脚本,包括数据库、maimai和thinking功能。 ## `src/` 目录结构 - **`plugins/` 目录**: 存放不同功能模块的插件。 - - **chat/**: 处理聊天相关的功能。 - - **memory_system/**: 处理机器人的记忆系统。 - - **personality/**: 处理机器人的性格系统。 - - **willing/**: 管理机器人的意愿系统。 + - **chat/**: 处理聊天相关的功能,如消息发送和接收。 + - **memory_system/**: 处理机器人的记忆功能。 + - **knowledege/**: 知识库相关功能。 - **models/**: 模型相关工具。 - - **schedule/**: 处理日程管理功能。 - - **moods/**: 情绪管理系统。 - - **zhishi/**: 知识库相关功能。 - - **remote/**: 远程控制功能。 - - **utils/**: 通用工具函数。 - - **config_reload/**: 配置热重载功能。 + - **schedule/**: 处理日程管理的功能。 - **`gui/` 目录**: 存放图形用户界面相关的代码。 + - **reasoning_gui.py**: 负责推理界面的实现,提供用户交互。 - **`common/` 目录**: 存放通用的工具和库。 + - **database.py**: 处理与数据库的交互,负责数据的存储和检索。 + - ****init**.py**: 初始化模块。 -- **`think_flow_demo/` 目录**: 思维流程演示相关代码。 +## `config/` 目录 -## 新增特色功能 +- **bot_config_template.toml**: 机器人配置模板。 +- **auto_format.py**: 自动格式化工具。 -1. **WebUI系统**: - - 提供图形化操作界面 - - 支持实时监控和控制 - - 可视化配置管理 +### `src/plugins/chat/` 目录文件详细介绍 -2. **多模式启动支持**: - - 开发环境(run_dev.bat) - - 生产环境 - - WebUI模式(webui_conda.bat) - - 记忆可视化(run_memory_vis.bat) +1. **`__init__.py`**: + - 初始化 `chat` 模块,使其可以作为一个包被导入。 -3. **增强的情感系统**: - - 情绪管理(moods插件) - - 性格系统(personality插件) - - 意愿系统(willing插件) +2. **`bot.py`**: + - 主要的聊天机器人逻辑实现,处理消息的接收、思考和回复。 + - 包含 `ChatBot` 类,负责消息处理流程控制。 + - 集成记忆系统和意愿管理。 -4. **远程控制功能**: - - 支持远程操作和监控 - - 分布式部署支持 +3. **`config.py`**: + - 配置文件,定义了聊天机器人的各种参数和设置。 + - 包含 `BotConfig` 和全局配置对象 `global_config`。 -5. **配置管理**: - - 支持配置热重载 - - 多环境配置(dev/prod) - - 自动配置更新检查 +4. **`cq_code.py`**: + - 处理 CQ 码(CoolQ 码),用于发送和接收特定格式的消息。 -6. **安全和隐私**: - - 用户协议(EULA)支持 - - 隐私政策遵守 - - 敏感信息保护 +5. **`emoji_manager.py`**: + - 管理表情包的发送和接收,根据情感选择合适的表情。 + - 提供根据情绪获取表情的方法。 -## 系统架构特点 +6. **`llm_generator.py`**: + - 生成基于大语言模型的回复,处理用户输入并生成相应的文本。 + - 通过 `ResponseGenerator` 类实现回复生成。 -1. **模块化设计**: - - 插件系统支持动态加载 - - 功能模块独立封装 - - 高度可扩展性 +7. **`message.py`**: + - 定义消息的结构和处理逻辑,包含多种消息类型: + - `Message`: 基础消息类 + - `MessageSet`: 消息集合 + - `Message_Sending`: 发送中的消息 + - `Message_Thinking`: 思考状态的消息 -2. **多层次AI交互**: - - 记忆系统 - - 情感系统 - - 知识库集成 - - 意愿管理 +8. **`message_sender.py`**: + - 控制消息的发送逻辑,确保消息按照特定规则发送。 + - 包含 `message_manager` 对象,用于管理消息队列。 -3. **完善的开发支持**: - - 开发环境配置 - - 代码规范检查 - - 自动化部署 - - Docker支持 +9. **`prompt_builder.py`**: + - 构建用于生成回复的提示,优化机器人的响应质量。 -4. **用户友好**: - - 图形化界面 - - 多种启动方式 - - 配置自动化 - - 详细的文档支持 +10. **`relationship_manager.py`**: + - 管理用户之间的关系,记录用户的互动和偏好。 + - 提供更新关系和关系值的方法。 + +11. **`Segment_builder.py`**: + - 构建消息片段的工具。 + +12. **`storage.py`**: + - 处理数据存储,负责将聊天记录和用户信息保存到数据库。 + - 实现 `MessageStorage` 类管理消息存储。 + +13. **`thinking_idea.py`**: + - 实现机器人的思考机制。 + +14. **`topic_identifier.py`**: + - 识别消息中的主题,帮助机器人理解用户的意图。 + +15. **`utils.py`** 和 **`utils_*.py`** 系列文件: + - 存放各种工具函数,提供辅助功能以支持其他模块。 + - 包括 `utils_cq.py`、`utils_image.py`、`utils_user.py` 等专门工具。 + +16. **`willing_manager.py`**: + - 管理机器人的回复意愿,动态调整回复概率。 + - 通过多种因素(如被提及、话题兴趣度)影响回复决策。 + +### `src/plugins/memory_system/` 目录文件介绍 + +1. **`memory.py`**: + - 实现记忆管理核心功能,包含 `memory_graph` 对象。 + - 提供相关项目检索,支持多层次记忆关联。 + +2. **`draw_memory.py`**: + - 记忆可视化工具。 + +3. **`memory_manual_build.py`**: + - 手动构建记忆的工具。 + +4. **`offline_llm.py`**: + - 离线大语言模型处理功能。 + +## 消息处理流程 + +### 1. 消息接收与预处理 + +- 通过 `ChatBot.handle_message()` 接收群消息。 +- 进行用户和群组的权限检查。 +- 更新用户关系信息。 +- 创建标准化的 `Message` 对象。 +- 对消息进行过滤和敏感词检测。 + +### 2. 主题识别与决策 + +- 使用 `topic_identifier` 识别消息主题。 +- 通过记忆系统检查对主题的兴趣度。 +- `willing_manager` 动态计算回复概率。 +- 根据概率决定是否回复消息。 + +### 3. 回复生成与发送 + +- 如需回复,首先创建 `Message_Thinking` 对象表示思考状态。 +- 调用 `ResponseGenerator.generate_response()` 生成回复内容和情感状态。 +- 删除思考消息,创建 `MessageSet` 准备发送回复。 +- 计算模拟打字时间,设置消息发送时间点。 +- 可能附加情感相关的表情包。 +- 通过 `message_manager` 将消息加入发送队列。 + +### 消息发送控制系统 + +`message_sender.py` 中实现了消息发送控制系统,采用三层结构: + +1. **消息管理**: + - 支持单条消息和消息集合的发送。 + - 处理思考状态消息,控制思考时间。 + - 模拟人类打字速度,添加自然发送延迟。 + +2. **情感表达**: + - 根据生成回复的情感状态选择匹配的表情包。 + - 通过 `emoji_manager` 管理表情资源。 + +3. **记忆交互**: + - 通过 `memory_graph` 检索相关记忆。 + - 根据记忆内容影响回复意愿和内容。 + +## 系统特色功能 + +1. **智能回复意愿系统**: + - 动态调整回复概率,模拟真实人类交流特性。 + - 考虑多种因素:被提及、话题兴趣度、用户关系等。 + +2. **记忆系统集成**: + - 支持多层次记忆关联和检索。 + - 影响机器人的兴趣和回复内容。 + +3. **自然交流模拟**: + - 模拟思考和打字过程,添加合理延迟。 + - 情感表达与表情包结合。 + +4. **多环境配置支持**: + - 支持开发环境和生产环境的不同配置。 + - 通过环境变量和配置文件灵活管理设置。 + +5. **Docker部署支持**: + - 提供容器化部署方案,简化安装和运行。 diff --git a/docs/docker_deploy.md b/docs/docker_deploy.md index f78f73dca..38eb54440 100644 --- a/docs/docker_deploy.md +++ b/docs/docker_deploy.md @@ -41,7 +41,7 @@ NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker-compose up -d ### 3. 修改配置并重启Docker -- 请前往 [🎀 新手配置指南](docs/installation_cute.md) 或 [⚙️ 标准配置指南](docs/installation_standard.md) 完成`.env.prod`与`bot_config.toml`配置文件的编写\ +- 请前往 [🎀 新手配置指南](./installation_cute.md) 或 [⚙️ 标准配置指南](./installation_standard.md) 完成`.env.prod`与`bot_config.toml`配置文件的编写\ **需要注意`.env.prod`中HOST处IP的填写,Docker中部署和系统中直接安装的配置会有所不同** - 重启Docker容器: diff --git a/src/plugins/chat/llm_generator.py b/src/plugins/chat/llm_generator.py index b9decdaa8..316260c87 100644 --- a/src/plugins/chat/llm_generator.py +++ b/src/plugins/chat/llm_generator.py @@ -35,7 +35,7 @@ class ResponseGenerator: request_type="response", ) self.model_v3 = LLM_request( - model=global_config.llm_normal, temperature=0.9, max_tokens=3000, request_type="response" + model=global_config.llm_normal, temperature=0.7, max_tokens=3000, request_type="response" ) self.model_r1_distill = LLM_request( model=global_config.llm_reasoning_minor, temperature=0.7, max_tokens=3000, request_type="response" diff --git a/src/plugins/willing/mode_classical.py b/src/plugins/willing/mode_classical.py index a0ec90ffc..155b2ba71 100644 --- a/src/plugins/willing/mode_classical.py +++ b/src/plugins/willing/mode_classical.py @@ -42,10 +42,9 @@ class WillingManager: interested_rate = interested_rate * config.response_interested_rate_amplifier - if interested_rate > 0.4: current_willing += interested_rate - 0.3 - + if is_mentioned_bot and current_willing < 1.0: current_willing += 1 elif is_mentioned_bot: From 88e160eb55d2b76c21fb8d5fb15129f701f9724d Mon Sep 17 00:00:00 2001 From: Noble Fish <89088785+DeathFishAtEase@users.noreply.github.com> Date: Wed, 26 Mar 2025 12:14:47 +0800 Subject: [PATCH 14/46] Update manual_deploy_windows.md --- docs/manual_deploy_windows.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/manual_deploy_windows.md b/docs/manual_deploy_windows.md index 37f0a5e31..d51151204 100644 --- a/docs/manual_deploy_windows.md +++ b/docs/manual_deploy_windows.md @@ -75,22 +75,22 @@ conda activate maimbot pip install -r requirements.txt ``` -### 2️⃣ **然后你需要启动MongoDB数据库,来存储信息** +### 3️⃣ **然后你需要启动MongoDB数据库,来存储信息** - 安装并启动MongoDB服务 - 默认连接本地27017端口 -### 3️⃣ **配置NapCat,让麦麦bot与qq取得联系** +### 4️⃣ **配置NapCat,让麦麦bot与qq取得联系** - 安装并登录NapCat(用你的qq小号) - 添加反向WS: `ws://127.0.0.1:8080/onebot/v11/ws` -### 4️⃣ **配置文件设置,让麦麦Bot正常工作** +### 5️⃣ **配置文件设置,让麦麦Bot正常工作** - 修改环境配置文件:`.env.prod` - 修改机器人配置文件:`bot_config.toml` -### 5️⃣ **启动麦麦机器人** +### 6️⃣ **启动麦麦机器人** - 打开命令行,cd到对应路径 @@ -104,7 +104,7 @@ nb run python bot.py ``` -### 6️⃣ **其他组件(可选)** +### 7️⃣ **其他组件(可选)** - `run_thingking.bat`: 启动可视化推理界面(未完善) - 直接运行 knowledge.py生成知识库 From 3c4f492b76d9a42eb6c637290bccbd6abead86ff Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 26 Mar 2025 12:51:39 +0800 Subject: [PATCH 15/46] =?UTF-8?q?fix=20=E6=80=9D=E7=BB=B4=E6=B5=81?= =?UTF-8?q?=E4=B8=8D=E4=BC=9A=E8=87=AA=E5=8A=A8=E5=81=9C=E6=AD=A2=E6=B6=88?= =?UTF-8?q?=E8=80=97token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/think_flow_demo/current_mind.py | 17 +++++++++++++---- src/think_flow_demo/heartflow.py | 4 ++-- template/bot_config_template.toml | 2 +- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/think_flow_demo/current_mind.py b/src/think_flow_demo/current_mind.py index 78447215f..6facdbf9b 100644 --- a/src/think_flow_demo/current_mind.py +++ b/src/think_flow_demo/current_mind.py @@ -4,6 +4,7 @@ from src.plugins.moods.moods import MoodManager from src.plugins.models.utils_model import LLM_request from src.plugins.chat.config import global_config import re +import time class CuttentState: def __init__(self): self.willing = 0 @@ -29,6 +30,8 @@ class SubHeartflow: self.observe_chat_id = None + self.last_reply_time = time.time() + if not self.current_mind: self.current_mind = "你什么也没想" @@ -38,10 +41,14 @@ class SubHeartflow: async def subheartflow_start_working(self): while True: - await self.do_a_thinking() - print("麦麦闹情绪了") - await self.judge_willing() - await asyncio.sleep(30) + current_time = time.time() + if current_time - self.last_reply_time > 180: # 3分钟 = 180秒 + # print(f"{self.observe_chat_id}麦麦已经3分钟没有回复了,暂时停止思考") + await asyncio.sleep(25) # 每30秒检查一次 + else: + await self.do_a_thinking() + await self.judge_willing() + await asyncio.sleep(25) async def do_a_thinking(self): print("麦麦小脑袋转起来了") @@ -99,6 +106,8 @@ class SubHeartflow: self.current_mind = reponse print(f"{self.observe_chat_id}麦麦的脑内状态:{self.current_mind}") + self.last_reply_time = time.time() + async def judge_willing(self): # print("麦麦闹情绪了1") personality_info = open("src/think_flow_demo/personality_info.txt", "r", encoding="utf-8").read() diff --git a/src/think_flow_demo/heartflow.py b/src/think_flow_demo/heartflow.py index c2e32d602..45843e490 100644 --- a/src/think_flow_demo/heartflow.py +++ b/src/think_flow_demo/heartflow.py @@ -30,7 +30,7 @@ class Heartflow: async def heartflow_start_working(self): while True: - await self.do_a_thinking() + # await self.do_a_thinking() await asyncio.sleep(60) async def do_a_thinking(self): @@ -82,7 +82,7 @@ class Heartflow: prompt = "" prompt += f"{personality_info}\n" - prompt += f"现在麦麦的想法是:{self.current_mind}\n" + prompt += f"现在{global_config.BOT_NICKNAME}的想法是:{self.current_mind}\n" prompt += f"现在麦麦在qq群里进行聊天,聊天的话题如下:{minds_str}\n" prompt += f"你现在{mood_info}\n" prompt += '''现在请你总结这些聊天内容,注意关注聊天内容对原有的想法的影响,输出连贯的内心独白 diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index e025df46c..6591d4272 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -132,7 +132,7 @@ enable = true [experimental] enable_friend_chat = false # 是否启用好友聊天 -enable_think_flow = false # 是否启用思维流 +enable_think_flow = false # 是否启用思维流 注意:可能会消耗大量token,请谨慎开启 #下面的模型若使用硅基流动则不需要更改,使用ds官方则改成.env.prod自定义的宏,使用自定义模型则选择定位相似的模型自己填写 #推理模型 From 07d891a9d79c91b96f647a92a4ffd7e8b3f63349 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 26 Mar 2025 13:37:49 +0800 Subject: [PATCH 16/46] Merge pull request #570 from Tianmoy/main-fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix:修复docs跳转错误 --- src/common/logger.py | 21 ++++++++++++++++++++- src/plugins/chat/__init__.py | 2 +- src/plugins/moods/moods.py | 9 +++++++-- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/common/logger.py b/src/common/logger.py index 91f1a1da0..45d6f4150 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -86,6 +86,25 @@ MEMORY_STYLE_CONFIG = { }, } + +#MOOD +MOOD_STYLE_CONFIG = { + "advanced": { + "console_format": ( + "{time:YYYY-MM-DD HH:mm:ss} | " + "{level: <8} | " + "{extra[module]: <12} | " + "心情 | " + "{message}" + ), + "file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 心情 | {message}"), + }, + "simple": { + "console_format": ("{time:MM-DD HH:mm} | 心情 | {message}"), + "file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 心情 | {message}"), + }, +} + SENDER_STYLE_CONFIG = { "advanced": { "console_format": ( @@ -163,7 +182,7 @@ TOPIC_STYLE_CONFIG = TOPIC_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else TOPIC_ST SENDER_STYLE_CONFIG = SENDER_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else SENDER_STYLE_CONFIG["advanced"] LLM_STYLE_CONFIG = LLM_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else LLM_STYLE_CONFIG["advanced"] CHAT_STYLE_CONFIG = CHAT_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else CHAT_STYLE_CONFIG["advanced"] - +MOOD_STYLE_CONFIG = MOOD_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else MOOD_STYLE_CONFIG["advanced"] def is_registered_module(record: dict) -> bool: """检查是否为已注册的模块""" diff --git a/src/plugins/chat/__init__.py b/src/plugins/chat/__init__.py index 78e026ca7..f51184a75 100644 --- a/src/plugins/chat/__init__.py +++ b/src/plugins/chat/__init__.py @@ -150,7 +150,7 @@ async def merge_memory_task(): # print("\033[1;32m[记忆整合]\033[0m 记忆整合完成") -@scheduler.scheduled_job("interval", seconds=30, id="print_mood") +@scheduler.scheduled_job("interval", seconds=15, id="print_mood") async def print_mood_task(): """每30秒打印一次情绪状态""" mood_manager = MoodManager.get_instance() diff --git a/src/plugins/moods/moods.py b/src/plugins/moods/moods.py index 3e977d024..986075da0 100644 --- a/src/plugins/moods/moods.py +++ b/src/plugins/moods/moods.py @@ -4,9 +4,14 @@ import time from dataclasses import dataclass from ..chat.config import global_config -from src.common.logger import get_module_logger +from src.common.logger import get_module_logger, LogConfig, MOOD_STYLE_CONFIG -logger = get_module_logger("mood_manager") +mood_config = LogConfig( + # 使用海马体专用样式 + console_format=MOOD_STYLE_CONFIG["console_format"], + file_format=MOOD_STYLE_CONFIG["file_format"], +) +logger = get_module_logger("mood_manager", config=mood_config) @dataclass From 1811e06c4f583262f9073d34593be2a60001aa54 Mon Sep 17 00:00:00 2001 From: Cookie987 Date: Wed, 26 Mar 2025 14:48:04 +0800 Subject: [PATCH 17/46] =?UTF-8?q?Linux=E4=B8=80=E9=94=AE=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E6=94=AF=E6=8C=81Arch/CentOS9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- run_debian12.sh => run.sh | 166 +++++++++++++++++++++++++++++++------- 1 file changed, 135 insertions(+), 31 deletions(-) rename run_debian12.sh => run.sh (72%) diff --git a/run_debian12.sh b/run.sh similarity index 72% rename from run_debian12.sh rename to run.sh index ae189844f..d34552fca 100644 --- a/run_debian12.sh +++ b/run.sh @@ -1,9 +1,10 @@ #!/bin/bash # 麦麦Bot一键安装脚本 by Cookie_987 -# 适用于Debian12 +# 适用于Arch/Ubuntu 24.10/Debian 12/CentOS 9 # 请小心使用任何一键脚本! +INSTALLER_VERSION="0.0.3" LANG=C.UTF-8 # 如无法访问GitHub请修改此处镜像地址 @@ -15,7 +16,14 @@ RED="\e[31m" RESET="\e[0m" # 需要的基本软件包 -REQUIRED_PACKAGES=("git" "sudo" "python3" "python3-venv" "curl" "gnupg" "python3-pip") + +declare -A REQUIRED_PACKAGES=( + ["common"]="git sudo python3 curl gnupg" + ["debian"]="python3-venv python3-pip" + ["ubuntu"]="python3-venv python3-pip" + ["centos"]="python3-pip" + ["arch"]="python-virtualenv python-pip" +) # 默认项目目录 DEFAULT_INSTALL_DIR="/opt/maimbot" @@ -28,8 +36,6 @@ IS_INSTALL_MONGODB=false IS_INSTALL_NAPCAT=false IS_INSTALL_DEPENDENCIES=false -INSTALLER_VERSION="0.0.1" - # 检查是否已安装 check_installed() { [[ -f /etc/systemd/system/${SERVICE_NAME}.service ]] @@ -193,6 +199,11 @@ check_eula() { # 首先计算当前隐私条款文件的哈希值 current_md5_privacy=$(md5sum "${INSTALL_DIR}/repo/PRIVACY.md" | awk '{print $1}') + # 如果当前的md5值为空,则直接返回 + if [[ -z $current_md5 || -z $current_md5_privacy ]]; then + whiptail --msgbox "🚫 未找到使用协议\n 请检查PRIVACY.md和EULA.md是否存在" 10 60 + fi + # 检查eula.confirmed文件是否存在 if [[ -f ${INSTALL_DIR}/repo/eula.confirmed ]]; then # 如果存在则检查其中包含的md5与current_md5是否一致 @@ -213,8 +224,8 @@ check_eula() { if [[ $current_md5 != $confirmed_md5 || $current_md5_privacy != $confirmed_md5_privacy ]]; then whiptail --title "📜 使用协议更新" --yesno "检测到麦麦Bot EULA或隐私条款已更新。\nhttps://github.com/SengokuCola/MaiMBot/blob/main/EULA.md\nhttps://github.com/SengokuCola/MaiMBot/blob/main/PRIVACY.md\n\n您是否同意上述协议? \n\n " 12 70 if [[ $? -eq 0 ]]; then - echo $current_md5 > ${INSTALL_DIR}/repo/eula.confirmed - echo $current_md5_privacy > ${INSTALL_DIR}/repo/privacy.confirmed + echo -n $current_md5 > ${INSTALL_DIR}/repo/eula.confirmed + echo -n $current_md5_privacy > ${INSTALL_DIR}/repo/privacy.confirmed else exit 1 fi @@ -227,7 +238,14 @@ run_installation() { # 1/6: 检测是否安装 whiptail if ! command -v whiptail &>/dev/null; then echo -e "${RED}[1/6] whiptail 未安装,正在安装...${RESET}" + + # 这里的多系统适配很神人,但是能用() + apt update && apt install -y whiptail + + pacman -S --noconfirm libnewt + + yum install -y newt fi # 协议确认 @@ -247,8 +265,18 @@ run_installation() { if [[ -f /etc/os-release ]]; then source /etc/os-release - if [[ "$ID" != "debian" || "$VERSION_ID" != "12" ]]; then - whiptail --title "🚫 不支持的系统" --msgbox "此脚本仅支持 Debian 12 (Bookworm)!\n当前系统: $PRETTY_NAME\n安装已终止。" 10 60 + if [[ "$ID" == "debian" && "$VERSION_ID" == "12" ]]; then + return + elif [[ "$ID" == "ubuntu" && "$VERSION_ID" == "24.10" ]]; then + return + elif [[ "$ID" == "centos" && "$VERSION_ID" == "9" ]]; then + return + elif [[ "$ID" == "arch" ]]; then + whiptail --title "⚠️ 兼容性警告" --msgbox "NapCat无可用的 Arch Linux 官方安装方法,将无法自动安装NapCat。\n\n您可尝试在AUR中搜索相关包。" 10 60 + whiptail --title "⚠️ 兼容性警告" --msgbox "MongoDB无可用的 Arch Linux 官方安装方法,将无法自动安装MongoDB。\n\n您可尝试在AUR中搜索相关包。" 10 60 + return + else + whiptail --title "🚫 不支持的系统" --msgbox "此脚本仅支持 Arch/Debian 12 (Bookworm)/Ubuntu 24.10 (Oracular Oriole)/CentOS9!\n当前系统: $PRETTY_NAME\n安装已终止。" 10 60 exit 1 fi else @@ -258,6 +286,20 @@ run_installation() { } check_system + # 设置包管理器 + case "$ID" in + debian|ubuntu) + PKG_MANAGER="apt" + ;; + centos) + PKG_MANAGER="yum" + ;; + arch) + # 添加arch包管理器 + PKG_MANAGER="pacman" + ;; + esac + # 检查MongoDB check_mongodb() { if command -v mongod &>/dev/null; then @@ -281,18 +323,27 @@ run_installation() { # 安装必要软件包 install_packages() { missing_packages=() - for package in "${REQUIRED_PACKAGES[@]}"; do - if ! dpkg -s "$package" &>/dev/null; then - missing_packages+=("$package") - fi + # 检查 common 及当前系统专属依赖 + for package in ${REQUIRED_PACKAGES["common"]} ${REQUIRED_PACKAGES["$ID"]}; do + case "$PKG_MANAGER" in + apt) + dpkg -s "$package" &>/dev/null || missing_packages+=("$package") + ;; + yum) + rpm -q "$package" &>/dev/null || missing_packages+=("$package") + ;; + pacman) + pacman -Qi "$package" &>/dev/null || missing_packages+=("$package") + ;; + esac done if [[ ${#missing_packages[@]} -gt 0 ]]; then - whiptail --title "📦 [3/6] 软件包检查" --yesno "检测到以下必须的依赖项目缺失:\n${missing_packages[*]}\n\n是否要自动安装?" 12 60 + whiptail --title "📦 [3/6] 依赖检查" --yesno "以下软件包缺失:\n${missing_packages[*]}\n\n是否自动安装?" 10 60 if [[ $? -eq 0 ]]; then IS_INSTALL_DEPENDENCIES=true else - whiptail --title "⚠️ 注意" --yesno "某些必要的依赖项未安装,可能会影响运行!\n是否继续?" 10 60 || exit 1 + whiptail --title "⚠️ 注意" --yesno "未安装某些依赖,可能影响运行!\n是否继续?" 10 60 || exit 1 fi fi } @@ -302,27 +353,24 @@ run_installation() { install_mongodb() { [[ $MONGO_INSTALLED == true ]] && return whiptail --title "📦 [3/6] 软件包检查" --yesno "检测到未安装MongoDB,是否安装?\n如果您想使用远程数据库,请跳过此步。" 10 60 && { - echo -e "${GREEN}安装 MongoDB...${RESET}" - curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg --dearmor - echo "deb [ signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] http://repo.mongodb.org/apt/debian bookworm/mongodb-org/8.0 main" | tee /etc/apt/sources.list.d/mongodb-org-8.0.list - apt update - apt install -y mongodb-org - systemctl enable --now mongod IS_INSTALL_MONGODB=true } } - install_mongodb + + # 仅在非Arch系统上安装MongoDB + [[ "$ID" != "arch" ]] && install_mongodb + # 安装NapCat install_napcat() { [[ $NAPCAT_INSTALLED == true ]] && return whiptail --title "📦 [3/6] 软件包检查" --yesno "检测到未安装NapCat,是否安装?\n如果您想使用远程NapCat,请跳过此步。" 10 60 && { - echo -e "${GREEN}安装 NapCat...${RESET}" - curl -o napcat.sh https://nclatest.znin.net/NapNeko/NapCat-Installer/main/script/install.sh && bash napcat.sh --cli y --docker n IS_INSTALL_NAPCAT=true } } - install_napcat + + # 仅在非Arch系统上安装NapCat + [[ "$ID" != "arch" ]] && install_napcat # Python版本检查 check_python() { @@ -332,7 +380,12 @@ run_installation() { exit 1 fi } - check_python + + # 如果没安装python则不检查python版本 + if command -v python3 &>/dev/null; then + check_python + fi + # 选择分支 choose_branch() { @@ -358,20 +411,71 @@ run_installation() { local confirm_msg="请确认以下信息:\n\n" confirm_msg+="📂 安装麦麦Bot到: $INSTALL_DIR\n" confirm_msg+="🔀 分支: $BRANCH\n" - [[ $IS_INSTALL_DEPENDENCIES == true ]] && confirm_msg+="📦 安装依赖:${missing_packages}\n" + [[ $IS_INSTALL_DEPENDENCIES == true ]] && confirm_msg+="📦 安装依赖:${missing_packages[@]}\n" [[ $IS_INSTALL_MONGODB == true || $IS_INSTALL_NAPCAT == true ]] && confirm_msg+="📦 安装额外组件:\n" [[ $IS_INSTALL_MONGODB == true ]] && confirm_msg+=" - MongoDB\n" [[ $IS_INSTALL_NAPCAT == true ]] && confirm_msg+=" - NapCat\n" confirm_msg+="\n注意:本脚本默认使用ghfast.top为GitHub进行加速,如不想使用请手动修改脚本开头的GITHUB_REPO变量。" - whiptail --title "🔧 安装确认" --yesno "$confirm_msg" 16 60 || exit 1 + whiptail --title "🔧 安装确认" --yesno "$confirm_msg" 20 60 || exit 1 } confirm_install # 开始安装 - echo -e "${GREEN}安装依赖...${RESET}" - [[ $IS_INSTALL_DEPENDENCIES == true ]] && apt update && apt install -y "${missing_packages[@]}" + echo -e "${GREEN}安装${missing_packages[@]}...${RESET}" + + if [[ $IS_INSTALL_DEPENDENCIES == true ]]; then + case "$PKG_MANAGER" in + apt) + apt update && apt install -y "${missing_packages[@]}" + ;; + yum) + yum install -y "${missing_packages[@]}" --nobest + ;; + pacman) + pacman -S --noconfirm "${missing_packages[@]}" + ;; + esac + fi + + if [[ $IS_INSTALL_MONGODB == true ]]; then + echo -e "${GREEN}安装 MongoDB...${RESET}" + case "$ID" in + debian) + curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg --dearmor + echo "deb [ signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] http://repo.mongodb.org/apt/debian bookworm/mongodb-org/8.0 main" | tee /etc/apt/sources.list.d/mongodb-org-8.0.list + apt update + apt install -y mongodb-org + systemctl enable --now mongod + ;; + ubuntu) + curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg --dearmor + echo "deb [ signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] http://repo.mongodb.org/apt/debian bookworm/mongodb-org/8.0 main" | tee /etc/apt/sources.list.d/mongodb-org-8.0.list + apt update + apt install -y mongodb-org + systemctl enable --now mongod + ;; + centos) + cat > /etc/yum.repos.d/mongodb-org-8.0.repo < repo/eula.confirmed - echo $current_md5_privacy > repo/privacy.confirmed + echo -n $current_md5 > repo/eula.confirmed + echo -n $current_md5_privacy > repo/privacy.confirmed echo -e "${GREEN}创建系统服务...${RESET}" cat > /etc/systemd/system/${SERVICE_NAME}.service < Date: Wed, 26 Mar 2025 14:56:14 +0800 Subject: [PATCH 18/46] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b005bc189..f17f09ba8 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ MaiMBot是一个开源项目,我们非常欢迎你的参与。你的贡献, - 📦 **Windows 一键傻瓜式部署**:请运行项目根目录中的 `run.bat`,部署完成后请参照后续配置指南进行配置 -- 📦 Linux 自动部署(实验) :请下载并运行项目根目录中的`run.sh`并按照提示安装,部署完成后请参照后续配置指南进行配置 +- 📦 Linux 自动部署(Arch/CentOS9/Debian12/Ubuntu24.10) :请下载并运行项目根目录中的`run.sh`并按照提示安装,部署完成后请参照后续配置指南进行配置 - [📦 Windows 手动部署指南 ](docs/manual_deploy_windows.md) From f1003030c702aeb965170c2656ed832b4bc497b0 Mon Sep 17 00:00:00 2001 From: HYY1116 Date: Wed, 26 Mar 2025 15:01:25 +0800 Subject: [PATCH 19/46] =?UTF-8?q?feat:=20=E5=9C=A8=E7=BA=BF=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E5=A2=9E=E5=8A=A0=E7=89=88=E6=9C=AC=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/remote/remote.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/plugins/remote/remote.py b/src/plugins/remote/remote.py index fdc805df1..8586aa67a 100644 --- a/src/plugins/remote/remote.py +++ b/src/plugins/remote/remote.py @@ -54,7 +54,9 @@ def send_heartbeat(server_url, client_id): sys = platform.system() try: headers = {"Client-ID": client_id, "User-Agent": f"HeartbeatClient/{client_id[:8]}"} - data = json.dumps({"system": sys}) + data = json.dumps( + {"system": sys, "Version": global_config.MAI_VERSION}, + ) response = requests.post(f"{server_url}/api/clients", headers=headers, data=data) if response.status_code == 201: @@ -92,9 +94,9 @@ class HeartbeatThread(threading.Thread): logger.info(f"{self.interval}秒后发送下一次心跳...") else: logger.info(f"{self.interval}秒后重试...") - + self.last_heartbeat_time = time.time() - + # 使用可中断的等待代替 sleep # 每秒检查一次是否应该停止或发送心跳 remaining_wait = self.interval @@ -104,7 +106,7 @@ class HeartbeatThread(threading.Thread): if self.stop_event.wait(wait_time): break # 如果事件被设置,立即退出等待 remaining_wait -= wait_time - + # 检查是否由于外部原因导致间隔异常延长 if time.time() - self.last_heartbeat_time >= self.interval * 1.5: logger.warning("检测到心跳间隔异常延长,立即发送心跳") From 6a805b7b22c636781f702b659fa611e7d9e1e7b0 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 26 Mar 2025 15:04:04 +0800 Subject: [PATCH 20/46] =?UTF-8?q?better=20=E6=96=B0=E6=97=A5=E7=A8=8B?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/schedule_generator copy.py | 191 --------------- .../schedule/schedule_generator_pro.py | 222 ++++++++++++++++++ 2 files changed, 222 insertions(+), 191 deletions(-) delete mode 100644 src/plugins/schedule/schedule_generator copy.py create mode 100644 src/plugins/schedule/schedule_generator_pro.py diff --git a/src/plugins/schedule/schedule_generator copy.py b/src/plugins/schedule/schedule_generator copy.py deleted file mode 100644 index eff0a08d6..000000000 --- a/src/plugins/schedule/schedule_generator copy.py +++ /dev/null @@ -1,191 +0,0 @@ -import datetime -import json -import re -import os -import sys -from typing import Dict, Union - - -# 添加项目根目录到 Python 路径 -root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) -sys.path.append(root_path) - -from src.common.database import db # noqa: E402 -from src.common.logger import get_module_logger # noqa: E402 -from src.plugins.schedule.offline_llm import LLMModel # noqa: E402 -from src.plugins.chat.config import global_config # noqa: E402 - -logger = get_module_logger("scheduler") - - -class ScheduleGenerator: - enable_output: bool = True - - def __init__(self): - # 使用离线LLM模型 - self.llm_scheduler = LLMModel(model_name="Pro/deepseek-ai/DeepSeek-V3", temperature=0.9) - self.today_schedule_text = "" - self.today_schedule = {} - self.tomorrow_schedule_text = "" - self.tomorrow_schedule = {} - self.yesterday_schedule_text = "" - self.yesterday_schedule = {} - - async def initialize(self): - today = datetime.datetime.now() - tomorrow = datetime.datetime.now() + datetime.timedelta(days=1) - yesterday = datetime.datetime.now() - datetime.timedelta(days=1) - - self.today_schedule_text, self.today_schedule = await self.generate_daily_schedule(target_date=today) - self.tomorrow_schedule_text, self.tomorrow_schedule = await self.generate_daily_schedule( - target_date=tomorrow, read_only=True - ) - self.yesterday_schedule_text, self.yesterday_schedule = await self.generate_daily_schedule( - target_date=yesterday, read_only=True - ) - - async def generate_daily_schedule( - self, target_date: datetime.datetime = None, read_only: bool = False - ) -> Dict[str, str]: - date_str = target_date.strftime("%Y-%m-%d") - weekday = target_date.strftime("%A") - - schedule_text = str - - existing_schedule = db.schedule.find_one({"date": date_str}) - if existing_schedule: - if self.enable_output: - logger.debug(f"{date_str}的日程已存在:") - schedule_text = existing_schedule["schedule"] - # print(self.schedule_text) - - elif not read_only: - logger.debug(f"{date_str}的日程不存在,准备生成新的日程。") - prompt = ( - f"""我是{global_config.BOT_NICKNAME},{global_config.PROMPT_SCHEDULE_GEN},请为我生成{date_str}({weekday})的日程安排,包括:""" - + """ - 1. 早上的学习和工作安排 - 2. 下午的活动和任务 - 3. 晚上的计划和休息时间 - 请按照时间顺序列出具体时间点和对应的活动,用一个时间点而不是时间段来表示时间,用JSON格式返回日程表, - 仅返回内容,不要返回注释,不要添加任何markdown或代码块样式,时间采用24小时制, - 格式为{"时间": "活动","时间": "活动",...}。""" - ) - - try: - schedule_text, _ = self.llm_scheduler.generate_response(prompt) - db.schedule.insert_one({"date": date_str, "schedule": schedule_text}) - self.enable_output = True - except Exception as e: - logger.error(f"生成日程失败: {str(e)}") - schedule_text = "生成日程时出错了" - # print(self.schedule_text) - else: - if self.enable_output: - logger.debug(f"{date_str}的日程不存在。") - schedule_text = "忘了" - - return schedule_text, None - - schedule_form = self._parse_schedule(schedule_text) - return schedule_text, schedule_form - - def _parse_schedule(self, schedule_text: str) -> Union[bool, Dict[str, str]]: - """解析日程文本,转换为时间和活动的字典""" - try: - reg = r"\{(.|\r|\n)+\}" - matched = re.search(reg, schedule_text)[0] - schedule_dict = json.loads(matched) - return schedule_dict - except json.JSONDecodeError: - logger.exception("解析日程失败: {}".format(schedule_text)) - return False - - def _parse_time(self, time_str: str) -> str: - """解析时间字符串,转换为时间""" - return datetime.datetime.strptime(time_str, "%H:%M") - - def get_current_task(self) -> str: - """获取当前时间应该进行的任务""" - current_time = datetime.datetime.now().strftime("%H:%M") - - # 找到最接近当前时间的任务 - closest_time = None - min_diff = float("inf") - - # 检查今天的日程 - if not self.today_schedule: - return "摸鱼" - for time_str in self.today_schedule.keys(): - diff = abs(self._time_diff(current_time, time_str)) - if closest_time is None or diff < min_diff: - closest_time = time_str - min_diff = diff - - # 检查昨天的日程中的晚间任务 - if self.yesterday_schedule: - for time_str in self.yesterday_schedule.keys(): - if time_str >= "20:00": # 只考虑晚上8点之后的任务 - # 计算与昨天这个时间点的差异(需要加24小时) - diff = abs(self._time_diff(current_time, time_str)) - if diff < min_diff: - closest_time = time_str - min_diff = diff - return closest_time, self.yesterday_schedule[closest_time] - - if closest_time: - return closest_time, self.today_schedule[closest_time] - return "摸鱼" - - def _time_diff(self, time1: str, time2: str) -> int: - """计算两个时间字符串之间的分钟差""" - if time1 == "24:00": - time1 = "23:59" - if time2 == "24:00": - time2 = "23:59" - t1 = datetime.datetime.strptime(time1, "%H:%M") - t2 = datetime.datetime.strptime(time2, "%H:%M") - diff = int((t2 - t1).total_seconds() / 60) - # 考虑时间的循环性 - if diff < -720: - diff += 1440 # 加一天的分钟 - elif diff > 720: - diff -= 1440 # 减一天的分钟 - # print(f"时间1[{time1}]: 时间2[{time2}],差值[{diff}]分钟") - return diff - - def print_schedule(self): - """打印完整的日程安排""" - if not self._parse_schedule(self.today_schedule_text): - logger.warning("今日日程有误,将在下次运行时重新生成") - db.schedule.delete_one({"date": datetime.datetime.now().strftime("%Y-%m-%d")}) - else: - logger.info("=== 今日日程安排 ===") - for time_str, activity in self.today_schedule.items(): - logger.info(f"时间[{time_str}]: 活动[{activity}]") - logger.info("==================") - self.enable_output = False - - -async def main(): - # 使用示例 - scheduler = ScheduleGenerator() - await scheduler.initialize() - scheduler.print_schedule() - print("\n当前任务:") - print(await scheduler.get_current_task()) - - print("昨天日程:") - print(scheduler.yesterday_schedule) - print("今天日程:") - print(scheduler.today_schedule) - print("明天日程:") - print(scheduler.tomorrow_schedule) - -# 当作为组件导入时使用的实例 -bot_schedule = ScheduleGenerator() - -if __name__ == "__main__": - import asyncio - # 当直接运行此文件时执行 - asyncio.run(main()) diff --git a/src/plugins/schedule/schedule_generator_pro.py b/src/plugins/schedule/schedule_generator_pro.py new file mode 100644 index 000000000..5a2c2a687 --- /dev/null +++ b/src/plugins/schedule/schedule_generator_pro.py @@ -0,0 +1,222 @@ +import datetime +import json +import re +import os +import sys +from typing import Dict, Union +# 添加项目根目录到 Python 路径 +root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) +sys.path.append(root_path) + +from src.common.database import db # noqa: E402 +from src.common.logger import get_module_logger # noqa: E402 +from src.plugins.schedule.offline_llm import LLMModel # noqa: E402 + +logger = get_module_logger("scheduler") + + +class ScheduleGenerator: + enable_output: bool = True + + def __init__(self, name: str = "bot_name", personality: str = "你是一个爱国爱党的新时代青年", behavior: str = "你非常外向,喜欢尝试新事物和人交流"): + # 使用离线LLM模型 + self.llm_scheduler = LLMModel(model_name="Pro/deepseek-ai/DeepSeek-V3", temperature=0.9) + + self.today_schedule_text = "" + self.today_done_list = [] + + self.yesterday_schedule_text = "" + self.yesterday_done_list = [] + + self.name = name + self.personality = personality + self.behavior = behavior + + self.start_time = datetime.datetime.now() + + async def mai_schedule_start(self): + """启动日程系统,每5分钟执行一次move_doing,并在日期变化时重新检查日程""" + try: + logger.info(f"日程系统启动/刷新时间: {self.start_time.strftime('%Y-%m-%d %H:%M:%S')}") + # 初始化日程 + await self.check_and_create_today_schedule() + self.print_schedule() + + while True: + current_time = datetime.datetime.now() + + # 检查是否需要重新生成日程(日期变化) + if current_time.date() != self.start_time.date(): + logger.info("检测到日期变化,重新生成日程") + self.start_time = current_time + await self.check_and_create_today_schedule() + self.print_schedule() + + # 执行当前活动 + current_activity = await self.move_doing() + logger.info(f"当前活动: {current_activity}") + + # 等待5分钟 + await asyncio.sleep(300) # 300秒 = 5分钟 + + except Exception as e: + logger.error(f"日程系统运行时出错: {str(e)}") + logger.exception("详细错误信息:") + + async def check_and_create_today_schedule(self): + """检查昨天的日程,并确保今天有日程安排 + + Returns: + tuple: (today_schedule_text, today_schedule) 今天的日程文本和解析后的日程字典 + """ + today = datetime.datetime.now() + yesterday = today - datetime.timedelta(days=1) + + # 先检查昨天的日程 + self.yesterday_schedule_text, self.yesterday_done_list = self.load_schedule_from_db(yesterday) + if self.yesterday_schedule_text: + logger.debug(f"已加载{yesterday.strftime('%Y-%m-%d')}的日程") + + # 检查今天的日程 + self.today_schedule_text, self.today_done_list = self.load_schedule_from_db(today) + if not self.today_schedule_text: + logger.info(f"{today.strftime('%Y-%m-%d')}的日程不存在,准备生成新的日程") + self.today_schedule_text = await self.generate_daily_schedule(target_date=today) + + self.save_today_schedule_to_db() + + def construct_daytime_prompt(self, target_date: datetime.datetime): + date_str = target_date.strftime("%Y-%m-%d") + weekday = target_date.strftime("%A") + + prompt = f"我是{self.name},{self.personality},{self.behavior}" + prompt += f"我昨天的日程是:{self.yesterday_schedule_text}\n" + prompt += f"请为我生成{date_str}({weekday})的日程安排,结合我的个人特点和行为习惯\n" + prompt += "推测我的日程安排,包括我一天都在做什么,有什么发现和思考,具体一些,详细一些,记得写明时间\n" + prompt += "直接返回我的日程,不要输出其他内容:" + return prompt + + def construct_doing_prompt(self,time: datetime.datetime): + now_time = time.strftime("%H:%M") + previous_doing = self.today_done_list[-20:] if len(self.today_done_list) > 20 else self.today_done_list + prompt = f"我是{self.name},{self.personality},{self.behavior}" + prompt += f"我今天的日程是:{self.today_schedule_text}\n" + prompt += f"我之前做了的事情是:{previous_doing}\n" + prompt += f"现在是{now_time},结合我的个人特点和行为习惯," + prompt += "推测我现在做什么,具体一些,详细一些\n" + prompt += "直接返回我在做的事情,不要输出其他内容:" + return prompt + + async def generate_daily_schedule( + self, target_date: datetime.datetime = None,) -> Dict[str, str]: + daytime_prompt = self.construct_daytime_prompt(target_date) + daytime_response, _ = await self.llm_scheduler.generate_response(daytime_prompt) + return daytime_response + + def _time_diff(self, time1: str, time2: str) -> int: + """计算两个时间字符串之间的分钟差""" + if time1 == "24:00": + time1 = "23:59" + if time2 == "24:00": + time2 = "23:59" + t1 = datetime.datetime.strptime(time1, "%H:%M") + t2 = datetime.datetime.strptime(time2, "%H:%M") + diff = int((t2 - t1).total_seconds() / 60) + # 考虑时间的循环性 + if diff < -720: + diff += 1440 # 加一天的分钟 + elif diff > 720: + diff -= 1440 # 减一天的分钟 + # print(f"时间1[{time1}]: 时间2[{time2}],差值[{diff}]分钟") + return diff + + def print_schedule(self): + """打印完整的日程安排""" + if not self.today_schedule_text: + logger.warning("今日日程有误,将在下次运行时重新生成") + db.schedule.delete_one({"date": datetime.datetime.now().strftime("%Y-%m-%d")}) + else: + logger.info("=== 今日日程安排 ===") + logger.info(self.today_schedule_text) + logger.info("==================") + self.enable_output = False + + async def update_today_done_list(self): + # 更新数据库中的 today_done_list + today_str = datetime.datetime.now().strftime("%Y-%m-%d") + existing_schedule = db.schedule.find_one({"date": today_str}) + + if existing_schedule: + # 更新数据库中的 today_done_list + db.schedule.update_one( + {"date": today_str}, + {"$set": {"today_done_list": self.today_done_list}} + ) + logger.debug(f"已更新{today_str}的已完成活动列表") + else: + logger.warning(f"未找到{today_str}的日程记录") + + async def move_doing(self): + current_time = datetime.datetime.now() + time_str = current_time.strftime("%H:%M") + doing_prompt = self.construct_doing_prompt(current_time) + doing_response, _ = await self.llm_scheduler.generate_response(doing_prompt) + self.today_done_list.append(current_time,time_str + "在" + doing_response) + + await self.update_today_done_list() + + return doing_response + + + + + def save_today_schedule_to_db(self): + """保存日程到数据库,同时初始化 today_done_list""" + date_str = datetime.datetime.now().strftime("%Y-%m-%d") + schedule_data = { + "date": date_str, + "schedule": self.today_schedule_text, + "today_done_list": self.today_done_list if hasattr(self, 'today_done_list') else [] + } + # 使用 upsert 操作,如果存在则更新,不存在则插入 + db.schedule.update_one( + {"date": date_str}, + {"$set": schedule_data}, + upsert=True + ) + logger.debug(f"已保存{date_str}的日程到数据库") + + def load_schedule_from_db(self, date: datetime.datetime): + """从数据库加载日程,同时加载 today_done_list""" + date_str = date.strftime("%Y-%m-%d") + existing_schedule = db.schedule.find_one({"date": date_str}) + + if existing_schedule: + schedule_text = existing_schedule["schedule"] + return schedule_text, existing_schedule.get("today_done_list", []) + else: + logger.debug(f"{date_str}的日程不存在") + return None, None + +async def main(): + # 使用示例 + scheduler = ScheduleGenerator(name="麦麦", personality="你叫麦麦,你19岁,是一个大二的女大学生,你有一头黑色短发,你会刷贴吧,你现在在学习心理学", behavior="你比较内向") + await scheduler.check_and_create_today_schedule() + scheduler.print_schedule() + print("\n当前任务:") + print(await scheduler.get_current_task()) + + print("昨天日程:") + print(scheduler.yesterday_schedule) + print("今天日程:") + print(scheduler.today_schedule) + print("明天日程:") + print(scheduler.tomorrow_schedule) + +# 当作为组件导入时使用的实例 +bot_schedule = ScheduleGenerator() + +if __name__ == "__main__": + import asyncio + # 当直接运行此文件时执行 + asyncio.run(main()) From ee4a2f6e72a81fcdb46122a7bb8856b80aca9891 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 26 Mar 2025 21:31:32 +0800 Subject: [PATCH 21/46] =?UTF-8?q?better=20=E6=97=A5=E7=A8=8B=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/plugins/schedule/offline_llm.py | 52 +----------- .../schedule/schedule_generator_pro.py | 82 ++++++++++++++----- 3 files changed, 64 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index b005bc189..8dfbbe430 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ MaiMBot是一个开源项目,我们非常欢迎你的参与。你的贡献, ### 💬交流群 - [五群](https://qm.qq.com/q/JxvHZnxyec) 1022489779(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 - [一群](https://qm.qq.com/q/VQ3XZrWgMs) 766798517 【已满】(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 -- [二群](https://qm.qq.com/q/RzmCiRtHEW) 571780722 【已满】(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 +- [二群](https://qm.qq.com/q/RzmCiRtHEW) 571780722(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 - [三群](https://qm.qq.com/q/wlH5eT8OmQ) 1035228475【已满】(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 - [四群](https://qm.qq.com/q/wlH5eT8OmQ) 729957033【已满】(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 diff --git a/src/plugins/schedule/offline_llm.py b/src/plugins/schedule/offline_llm.py index e4dc23f93..5c56d9e00 100644 --- a/src/plugins/schedule/offline_llm.py +++ b/src/plugins/schedule/offline_llm.py @@ -22,57 +22,7 @@ class LLMModel: logger.info(f"API URL: {self.base_url}") # 使用 logger 记录 base_url - def generate_response(self, prompt: str) -> Union[str, Tuple[str, str]]: - """根据输入的提示生成模型的响应""" - headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"} - - # 构建请求体 - data = { - "model": self.model_name, - "messages": [{"role": "user", "content": prompt}], - "temperature": 0.5, - **self.params, - } - - # 发送请求到完整的 chat/completions 端点 - api_url = f"{self.base_url.rstrip('/')}/chat/completions" - logger.info(f"Request URL: {api_url}") # 记录请求的 URL - - max_retries = 3 - base_wait_time = 15 # 基础等待时间(秒) - - for retry in range(max_retries): - try: - response = requests.post(api_url, headers=headers, json=data) - - if response.status_code == 429: - wait_time = base_wait_time * (2**retry) # 指数退避 - logger.warning(f"遇到请求限制(429),等待{wait_time}秒后重试...") - time.sleep(wait_time) - continue - - response.raise_for_status() # 检查其他响应状态 - - result = response.json() - if "choices" in result and len(result["choices"]) > 0: - content = result["choices"][0]["message"]["content"] - reasoning_content = result["choices"][0]["message"].get("reasoning_content", "") - return content, reasoning_content - return "没有返回结果", "" - - except Exception as e: - if retry < max_retries - 1: # 如果还有重试机会 - wait_time = base_wait_time * (2**retry) - logger.error(f"[回复]请求失败,等待{wait_time}秒后重试... 错误: {str(e)}") - time.sleep(wait_time) - else: - logger.error(f"请求失败: {str(e)}") - return f"请求失败: {str(e)}", "" - - logger.error("达到最大重试次数,请求仍然失败") - return "达到最大重试次数,请求仍然失败", "" - - async def generate_response_async(self, prompt: str) -> Union[str, Tuple[str, str]]: + async def generate_response_async(self, prompt: str) -> str: """异步方式根据输入的提示生成模型的响应""" headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"} diff --git a/src/plugins/schedule/schedule_generator_pro.py b/src/plugins/schedule/schedule_generator_pro.py index 5a2c2a687..ceaf2afd2 100644 --- a/src/plugins/schedule/schedule_generator_pro.py +++ b/src/plugins/schedule/schedule_generator_pro.py @@ -20,7 +20,7 @@ class ScheduleGenerator: def __init__(self, name: str = "bot_name", personality: str = "你是一个爱国爱党的新时代青年", behavior: str = "你非常外向,喜欢尝试新事物和人交流"): # 使用离线LLM模型 - self.llm_scheduler = LLMModel(model_name="Pro/deepseek-ai/DeepSeek-V3", temperature=0.9) + self.llm_scheduler = LLMModel(model_name="Qwen/Qwen2.5-32B-Instruct", temperature=0.9) self.today_schedule_text = "" self.today_done_list = [] @@ -33,6 +33,8 @@ class ScheduleGenerator: self.behavior = behavior self.start_time = datetime.datetime.now() + + self.schedule_doing_update_interval = 60 #最好大于60 async def mai_schedule_start(self): """启动日程系统,每5分钟执行一次move_doing,并在日期变化时重新检查日程""" @@ -43,6 +45,8 @@ class ScheduleGenerator: self.print_schedule() while True: + print(self.get_current_num_task(1, True)) + current_time = datetime.datetime.now() # 检查是否需要重新生成日程(日期变化) @@ -56,8 +60,7 @@ class ScheduleGenerator: current_activity = await self.move_doing() logger.info(f"当前活动: {current_activity}") - # 等待5分钟 - await asyncio.sleep(300) # 300秒 = 5分钟 + await asyncio.sleep(self.schedule_doing_update_interval) except Exception as e: logger.error(f"日程系统运行时出错: {str(e)}") @@ -79,6 +82,8 @@ class ScheduleGenerator: # 检查今天的日程 self.today_schedule_text, self.today_done_list = self.load_schedule_from_db(today) + if not self.today_done_list: + self.today_done_list = [] if not self.today_schedule_text: logger.info(f"{today.strftime('%Y-%m-%d')}的日程不存在,准备生成新的日程") self.today_schedule_text = await self.generate_daily_schedule(target_date=today) @@ -98,10 +103,16 @@ class ScheduleGenerator: def construct_doing_prompt(self,time: datetime.datetime): now_time = time.strftime("%H:%M") - previous_doing = self.today_done_list[-20:] if len(self.today_done_list) > 20 else self.today_done_list + if self.today_done_list: + previous_doing = self.get_current_num_task(10, True) + print(previous_doing) + else: + previous_doing = "我没做什么事情" + + prompt = f"我是{self.name},{self.personality},{self.behavior}" prompt += f"我今天的日程是:{self.today_schedule_text}\n" - prompt += f"我之前做了的事情是:{previous_doing}\n" + prompt += f"我之前做了的事情是:{previous_doing},从之前到现在已经过去了{self.schedule_doing_update_interval/60}分钟了\n" prompt += f"现在是{now_time},结合我的个人特点和行为习惯," prompt += "推测我现在做什么,具体一些,详细一些\n" prompt += "直接返回我在做的事情,不要输出其他内容:" @@ -110,7 +121,7 @@ class ScheduleGenerator: async def generate_daily_schedule( self, target_date: datetime.datetime = None,) -> Dict[str, str]: daytime_prompt = self.construct_daytime_prompt(target_date) - daytime_response, _ = await self.llm_scheduler.generate_response(daytime_prompt) + daytime_response,_ = await self.llm_scheduler.generate_response_async(daytime_prompt) return daytime_response def _time_diff(self, time1: str, time2: str) -> int: @@ -160,15 +171,54 @@ class ScheduleGenerator: current_time = datetime.datetime.now() time_str = current_time.strftime("%H:%M") doing_prompt = self.construct_doing_prompt(current_time) - doing_response, _ = await self.llm_scheduler.generate_response(doing_prompt) - self.today_done_list.append(current_time,time_str + "在" + doing_response) + doing_response,_ = await self.llm_scheduler.generate_response_async(doing_prompt) + self.today_done_list.append((current_time, time_str + "时," + doing_response)) await self.update_today_done_list() return doing_response + async def get_task_from_time_to_time(self, start_time: str, end_time: str): + """获取指定时间范围内的任务列表 + + Args: + start_time (str): 开始时间,格式为"HH:MM" + end_time (str): 结束时间,格式为"HH:MM" + + Returns: + list: 时间范围内的任务列表 + """ + result = [] + for task in self.today_done_list: + task_time = task[0] # 获取任务的时间戳 + task_time_str = task_time.strftime("%H:%M") + + # 检查任务时间是否在指定范围内 + if self._time_diff(start_time, task_time_str) >= 0 and self._time_diff(task_time_str, end_time) >= 0: + result.append(task) + + return result - + def get_current_num_task(self, num=1, time_info = False): + """获取最新加入的指定数量的日程 + + Args: + num (int): 需要获取的日程数量,默认为1 + + Returns: + list: 最新加入的日程列表 + """ + if not self.today_done_list: + return [] + + # 确保num不超过列表长度 + num = min(num, len(self.today_done_list)) + pre_doing = "" + for doing in self.today_done_list[-num:]: + pre_doing += doing[1] + + # 返回最新的num条日程 + return pre_doing def save_today_schedule_to_db(self): """保存日程到数据库,同时初始化 today_done_list""" @@ -200,18 +250,10 @@ class ScheduleGenerator: async def main(): # 使用示例 - scheduler = ScheduleGenerator(name="麦麦", personality="你叫麦麦,你19岁,是一个大二的女大学生,你有一头黑色短发,你会刷贴吧,你现在在学习心理学", behavior="你比较内向") - await scheduler.check_and_create_today_schedule() - scheduler.print_schedule() - print("\n当前任务:") - print(await scheduler.get_current_task()) + scheduler = ScheduleGenerator(name="麦麦", personality="你叫麦麦,你19岁,是一个大二的女大学生,你有一头黑色短发,你会刷贴吧,你现在在学习心理学", behavior="你比较内向,一般熬夜比较晚,然后第二天早上10点起床吃早午饭") + await scheduler.mai_schedule_start() + - print("昨天日程:") - print(scheduler.yesterday_schedule) - print("今天日程:") - print(scheduler.today_schedule) - print("明天日程:") - print(scheduler.tomorrow_schedule) # 当作为组件导入时使用的实例 bot_schedule = ScheduleGenerator() From 572bffc27355d56c83ff691b42793305bb6c701a Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 26 Mar 2025 22:42:19 +0800 Subject: [PATCH 22/46] =?UTF-8?q?better:=E6=97=A5=E5=BF=97=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E7=8E=B0=E5=B7=B2=E5=8F=AF=E4=BB=A5=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/logger.py | 18 + src/plugins/chat/__init__.py | 22 +- src/plugins/chat/config.py | 3 + src/plugins/chat/llm_generator.py | 6 +- src/plugins/chat/prompt_builder.py | 6 +- src/plugins/schedule/offline_llm.py | 2 +- src/plugins/schedule/schedule_generator.py | 387 +++++++++++------- .../schedule/schedule_generator_pro.py | 264 ------------ src/think_flow_demo/current_mind.py | 6 +- src/think_flow_demo/heartflow.py | 9 +- template/bot_config_template.toml | 1 + 11 files changed, 296 insertions(+), 428 deletions(-) delete mode 100644 src/plugins/schedule/schedule_generator_pro.py diff --git a/src/common/logger.py b/src/common/logger.py index 45d6f4150..b910427bf 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -122,6 +122,23 @@ SENDER_STYLE_CONFIG = { }, } +SCHEDULE_STYLE_CONFIG = { + "advanced": { + "console_format": ( + "{time:YYYY-MM-DD HH:mm:ss} | " + "{level: <8} | " + "{extra[module]: <12} | " + "在干嘛 | " + "{message}" + ), + "file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 在干嘛 | {message}"), + }, + "simple": { + "console_format": ("{time:MM-DD HH:mm} | 在干嘛 | {message}"), + "file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 在干嘛 | {message}"), + }, +} + LLM_STYLE_CONFIG = { "advanced": { "console_format": ( @@ -183,6 +200,7 @@ SENDER_STYLE_CONFIG = SENDER_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else SENDER LLM_STYLE_CONFIG = LLM_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else LLM_STYLE_CONFIG["advanced"] CHAT_STYLE_CONFIG = CHAT_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else CHAT_STYLE_CONFIG["advanced"] MOOD_STYLE_CONFIG = MOOD_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else MOOD_STYLE_CONFIG["advanced"] +SCHEDULE_STYLE_CONFIG = SCHEDULE_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else SCHEDULE_STYLE_CONFIG["advanced"] def is_registered_module(record: dict) -> bool: """检查是否为已注册的模块""" diff --git a/src/plugins/chat/__init__.py b/src/plugins/chat/__init__.py index f51184a75..8bbb16bf5 100644 --- a/src/plugins/chat/__init__.py +++ b/src/plugins/chat/__init__.py @@ -79,10 +79,14 @@ async def start_background_tasks(): # 只启动表情包管理任务 asyncio.create_task(emoji_manager.start_periodic_check()) - await bot_schedule.initialize() - bot_schedule.print_schedule() +@driver.on_startup +async def init_schedule(): + """在 NoneBot2 启动时初始化日程系统""" + bot_schedule.initialize(name=global_config.BOT_NICKNAME, personality=global_config.PROMPT_PERSONALITY, behavior=global_config.PROMPT_SCHEDULE_GEN, interval=global_config.SCHEDULE_DOING_UPDATE_INTERVAL) + asyncio.create_task(bot_schedule.mai_schedule_start()) + @driver.on_startup async def init_relationships(): """在 NoneBot2 启动时初始化关系管理器""" @@ -157,13 +161,13 @@ async def print_mood_task(): mood_manager.print_mood_status() -@scheduler.scheduled_job("interval", seconds=7200, id="generate_schedule") -async def generate_schedule_task(): - """每2小时尝试生成一次日程""" - logger.debug("尝试生成日程") - await bot_schedule.initialize() - if not bot_schedule.enable_output: - bot_schedule.print_schedule() +# @scheduler.scheduled_job("interval", seconds=7200, id="generate_schedule") +# async def generate_schedule_task(): +# """每2小时尝试生成一次日程""" +# logger.debug("尝试生成日程") +# await bot_schedule.initialize() +# if not bot_schedule.enable_output: +# bot_schedule.print_schedule() @scheduler.scheduled_job("interval", seconds=3600, id="remove_recalled_message") diff --git a/src/plugins/chat/config.py b/src/plugins/chat/config.py index 2d9badbc0..1ac2a7ea5 100644 --- a/src/plugins/chat/config.py +++ b/src/plugins/chat/config.py @@ -42,6 +42,7 @@ class BotConfig: # schedule ENABLE_SCHEDULE_GEN: bool = False # 是否启用日程生成 PROMPT_SCHEDULE_GEN = "无日程" + SCHEDULE_DOING_UPDATE_INTERVAL: int = 300 # 日程表更新间隔 单位秒 # message MAX_CONTEXT_SIZE: int = 15 # 上下文最大消息数 @@ -219,6 +220,8 @@ class BotConfig: schedule_config = parent["schedule"] config.ENABLE_SCHEDULE_GEN = schedule_config.get("enable_schedule_gen", config.ENABLE_SCHEDULE_GEN) config.PROMPT_SCHEDULE_GEN = schedule_config.get("prompt_schedule_gen", config.PROMPT_SCHEDULE_GEN) + config.SCHEDULE_DOING_UPDATE_INTERVAL = schedule_config.get( + "schedule_doing_update_interval", config.SCHEDULE_DOING_UPDATE_INTERVAL) logger.info( f"载入自定义日程prompt:{schedule_config.get('prompt_schedule_gen', config.PROMPT_SCHEDULE_GEN)}") diff --git a/src/plugins/chat/llm_generator.py b/src/plugins/chat/llm_generator.py index 316260c87..7b032104a 100644 --- a/src/plugins/chat/llm_generator.py +++ b/src/plugins/chat/llm_generator.py @@ -51,13 +51,13 @@ class ResponseGenerator: # 从global_config中获取模型概率值并选择模型 rand = random.random() if rand < global_config.MODEL_R1_PROBABILITY: - self.current_model_type = "r1" + self.current_model_type = "深深地" current_model = self.model_r1 elif rand < global_config.MODEL_R1_PROBABILITY + global_config.MODEL_V3_PROBABILITY: - self.current_model_type = "v3" + self.current_model_type = "浅浅的" current_model = self.model_v3 else: - self.current_model_type = "r1_distill" + self.current_model_type = "又浅又浅的" current_model = self.model_r1_distill logger.info(f"{global_config.BOT_NICKNAME}{self.current_model_type}思考中") diff --git a/src/plugins/chat/prompt_builder.py b/src/plugins/chat/prompt_builder.py index ef070ed24..283ea0eae 100644 --- a/src/plugins/chat/prompt_builder.py +++ b/src/plugins/chat/prompt_builder.py @@ -57,9 +57,7 @@ class PromptBuilder: mood_prompt = mood_manager.get_prompt() # 日程构建 - # current_date = time.strftime("%Y-%m-%d", time.localtime()) - # current_time = time.strftime("%H:%M:%S", time.localtime()) - # bot_schedule_now_time, bot_schedule_now_activity = bot_schedule.get_current_task() + schedule_prompt = f'''你现在正在做的事情是:{bot_schedule.get_current_num_task(num = 1,time_info = False)}''' # 获取聊天上下文 chat_in_group = True @@ -173,8 +171,6 @@ class PromptBuilder: prompt_check_if_response = "" - # print(prompt) - return prompt, prompt_check_if_response def _build_initiative_prompt_select(self, group_id, probability_1=0.8, probability_2=0.1): diff --git a/src/plugins/schedule/offline_llm.py b/src/plugins/schedule/offline_llm.py index 5c56d9e00..f274740fa 100644 --- a/src/plugins/schedule/offline_llm.py +++ b/src/plugins/schedule/offline_llm.py @@ -30,7 +30,7 @@ class LLMModel: data = { "model": self.model_name, "messages": [{"role": "user", "content": prompt}], - "temperature": 0.5, + "temperature": 0.7, **self.params, } diff --git a/src/plugins/schedule/schedule_generator.py b/src/plugins/schedule/schedule_generator.py index b26b29549..d39b0517d 100644 --- a/src/plugins/schedule/schedule_generator.py +++ b/src/plugins/schedule/schedule_generator.py @@ -1,159 +1,149 @@ import datetime -import json -import re -from typing import Dict, Union - -from nonebot import get_driver - +import os +import sys +from typing import Dict +import asyncio # 添加项目根目录到 Python 路径 +root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) +sys.path.append(root_path) -from src.plugins.chat.config import global_config -from ...common.database import db # 使用正确的导入语法 -from ..models.utils_model import LLM_request -from src.common.logger import get_module_logger +from src.common.database import db # noqa: E402 +from src.common.logger import get_module_logger, SCHEDULE_STYLE_CONFIG, LogConfig # noqa: E402 +from src.plugins.models.utils_model import LLM_request # noqa: E402 +from src.plugins.chat.config import global_config # noqa: E402 -logger = get_module_logger("scheduler") -driver = get_driver() -config = driver.config +schedule_config = LogConfig( + # 使用海马体专用样式 + console_format=SCHEDULE_STYLE_CONFIG["console_format"], + file_format=SCHEDULE_STYLE_CONFIG["file_format"], +) +logger = get_module_logger("scheduler", config=schedule_config) class ScheduleGenerator: - enable_output: bool = True + # enable_output: bool = True - def __init__(self): - # 根据global_config.llm_normal这一字典配置指定模型 - # self.llm_scheduler = LLMModel(model = global_config.llm_normal,temperature=0.9) - self.llm_scheduler = LLM_request(model=global_config.llm_normal, temperature=0.9, request_type="scheduler") + def __init__(self, ): + # 使用离线LLM模型 + self.llm_scheduler_all = LLM_request( + model= global_config.llm_reasoning, temperature=0.9, max_tokens=2048,request_type="schedule") + self.llm_scheduler_doing = LLM_request( + model= global_config.llm_normal, temperature=0.9, max_tokens=2048,request_type="schedule") + self.today_schedule_text = "" - self.today_schedule = {} - self.tomorrow_schedule_text = "" - self.tomorrow_schedule = {} + self.today_done_list = [] + self.yesterday_schedule_text = "" - self.yesterday_schedule = {} + self.yesterday_done_list = [] - async def initialize(self): + self.name = "" + self.personality = "" + self.behavior = "" + + self.start_time = datetime.datetime.now() + + self.schedule_doing_update_interval = 60 #最好大于60 + + def initialize(self,name: str = "bot_name", personality: str = "你是一个爱国爱党的新时代青年", behavior: str = "你非常外向,喜欢尝试新事物和人交流",interval: int = 60): + """初始化日程系统""" + self.name = name + self.behavior = behavior + self.schedule_doing_update_interval = interval + + for pers in personality: + self.personality += pers + "\n" + + + async def mai_schedule_start(self): + """启动日程系统,每5分钟执行一次move_doing,并在日期变化时重新检查日程""" + try: + logger.info(f"日程系统启动/刷新时间: {self.start_time.strftime('%Y-%m-%d %H:%M:%S')}") + # 初始化日程 + await self.check_and_create_today_schedule() + self.print_schedule() + + while True: + print(self.get_current_num_task(1, True)) + + current_time = datetime.datetime.now() + + # 检查是否需要重新生成日程(日期变化) + if current_time.date() != self.start_time.date(): + logger.info("检测到日期变化,重新生成日程") + self.start_time = current_time + await self.check_and_create_today_schedule() + self.print_schedule() + + # 执行当前活动 + current_activity = await self.move_doing(mind_thinking="") + logger.info(f"当前活动: {current_activity}") + + await asyncio.sleep(self.schedule_doing_update_interval) + + except Exception as e: + logger.error(f"日程系统运行时出错: {str(e)}") + logger.exception("详细错误信息:") + + async def check_and_create_today_schedule(self): + """检查昨天的日程,并确保今天有日程安排 + + Returns: + tuple: (today_schedule_text, today_schedule) 今天的日程文本和解析后的日程字典 + """ today = datetime.datetime.now() - tomorrow = datetime.datetime.now() + datetime.timedelta(days=1) - yesterday = datetime.datetime.now() - datetime.timedelta(days=1) + yesterday = today - datetime.timedelta(days=1) + + # 先检查昨天的日程 + self.yesterday_schedule_text, self.yesterday_done_list = self.load_schedule_from_db(yesterday) + if self.yesterday_schedule_text: + logger.debug(f"已加载{yesterday.strftime('%Y-%m-%d')}的日程") + + # 检查今天的日程 + self.today_schedule_text, self.today_done_list = self.load_schedule_from_db(today) + if not self.today_done_list: + self.today_done_list = [] + if not self.today_schedule_text: + logger.info(f"{today.strftime('%Y-%m-%d')}的日程不存在,准备生成新的日程") + self.today_schedule_text = await self.generate_daily_schedule(target_date=today) - self.today_schedule_text, self.today_schedule = await self.generate_daily_schedule(target_date=today) - self.tomorrow_schedule_text, self.tomorrow_schedule = await self.generate_daily_schedule( - target_date=tomorrow, read_only=True - ) - self.yesterday_schedule_text, self.yesterday_schedule = await self.generate_daily_schedule( - target_date=yesterday, read_only=True - ) - - async def generate_daily_schedule( - self, target_date: datetime.datetime = None, read_only: bool = False - ) -> Dict[str, str]: + self.save_today_schedule_to_db() + + def construct_daytime_prompt(self, target_date: datetime.datetime): date_str = target_date.strftime("%Y-%m-%d") weekday = target_date.strftime("%A") - schedule_text = str - - existing_schedule = db.schedule.find_one({"date": date_str}) - if existing_schedule: - if self.enable_output: - logger.debug(f"{date_str}的日程已存在:") - schedule_text = existing_schedule["schedule"] - # print(self.schedule_text) - - elif not read_only: - logger.debug(f"{date_str}的日程不存在,准备生成新的日程。") - prompt = ( - f"""我是{global_config.BOT_NICKNAME},{global_config.PROMPT_SCHEDULE_GEN},请为我生成{date_str}({weekday})的日程安排,包括:""" - + """ - 1. 早上的学习和工作安排 - 2. 下午的活动和任务 - 3. 晚上的计划和休息时间 - 请按照时间顺序列出具体时间点和对应的活动,用一个时间点而不是时间段来表示时间,用JSON格式返回日程表, - 仅返回内容,不要返回注释,不要添加任何markdown或代码块样式,时间采用24小时制, - 格式为{"时间": "活动","时间": "活动",...}。""" - ) - - try: - schedule_text, _, _ = await self.llm_scheduler.generate_response(prompt) - db.schedule.insert_one({"date": date_str, "schedule": schedule_text}) - self.enable_output = True - except Exception as e: - logger.error(f"生成日程失败: {str(e)}") - schedule_text = "生成日程时出错了" - # print(self.schedule_text) + prompt = f"你是{self.name},{self.personality},{self.behavior}" + prompt += f"你昨天的日程是:{self.yesterday_schedule_text}\n" + prompt += f"请为你生成{date_str}({weekday})的日程安排,结合你的个人特点和行为习惯\n" + prompt += "推测你的日程安排,包括你一天都在做什么,有什么发现和思考,具体一些,详细一些,需要1500字以上,精确到每半个小时,记得写明时间\n" + prompt += "直接返回你的日程,不要输出其他内容:" + return prompt + + def construct_doing_prompt(self,time: datetime.datetime,mind_thinking: str = ""): + now_time = time.strftime("%H:%M") + if self.today_done_list: + previous_doings = self.get_current_num_task(10, True) + # print(previous_doings) else: - if self.enable_output: - logger.debug(f"{date_str}的日程不存在。") - schedule_text = "忘了" - - return schedule_text, None - - schedule_form = self._parse_schedule(schedule_text) - return schedule_text, schedule_form - - def _parse_schedule(self, schedule_text: str) -> Union[bool, Dict[str, str]]: - """解析日程文本,转换为时间和活动的字典""" - try: - reg = r"\{(.|\r|\n)+\}" - matched = re.search(reg, schedule_text)[0] - schedule_dict = json.loads(matched) - self._check_schedule_validity(schedule_dict) - return schedule_dict - except json.JSONDecodeError: - logger.exception("解析日程失败: {}".format(schedule_text)) - return False - except ValueError as e: - logger.exception(f"解析日程失败: {str(e)}") - return False - except Exception as e: - logger.exception(f"解析日程发生错误:{str(e)}") - return False - - def _check_schedule_validity(self, schedule_dict: Dict[str, str]): - """检查日程是否合法""" - if not schedule_dict: - return - for time_str in schedule_dict.keys(): - try: - self._parse_time(time_str) - except ValueError: - raise ValueError("日程时间格式不正确") from None - - def _parse_time(self, time_str: str) -> str: - """解析时间字符串,转换为时间""" - return datetime.datetime.strptime(time_str, "%H:%M") - - def get_current_task(self) -> str: - """获取当前时间应该进行的任务""" - current_time = datetime.datetime.now().strftime("%H:%M") - - # 找到最接近当前时间的任务 - closest_time = None - min_diff = float("inf") - - # 检查今天的日程 - if not self.today_schedule: - return "摸鱼" - for time_str in self.today_schedule.keys(): - diff = abs(self._time_diff(current_time, time_str)) - if closest_time is None or diff < min_diff: - closest_time = time_str - min_diff = diff - - # 检查昨天的日程中的晚间任务 - if self.yesterday_schedule: - for time_str in self.yesterday_schedule.keys(): - if time_str >= "20:00": # 只考虑晚上8点之后的任务 - # 计算与昨天这个时间点的差异(需要加24小时) - diff = abs(self._time_diff(current_time, time_str)) - if diff < min_diff: - closest_time = time_str - min_diff = diff - return closest_time, self.yesterday_schedule[closest_time] - - if closest_time: - return closest_time, self.today_schedule[closest_time] - return "摸鱼" + previous_doings = "你没做什么事情" + + + prompt = f"你是{self.name},{self.personality},{self.behavior}" + prompt += f"你今天的日程是:{self.today_schedule_text}\n" + if mind_thinking: + prompt += f"你脑子里在想:{mind_thinking}\n" + prompt += f"你之前做了的事情是:{previous_doings},从之前到现在已经过去了{self.schedule_doing_update_interval/60}分钟了\n" + prompt += f"现在是{now_time},结合你的个人特点和行为习惯," + prompt += "推测你现在做什么,具体一些,详细一些\n" + prompt += "直接返回你在做的事情,不要输出其他内容:" + return prompt + + async def generate_daily_schedule( + self, target_date: datetime.datetime = None,) -> Dict[str, str]: + daytime_prompt = self.construct_daytime_prompt(target_date) + daytime_response,_ = await self.llm_scheduler_all.generate_response_async(daytime_prompt) + return daytime_response def _time_diff(self, time1: str, time2: str) -> int: """计算两个时间字符串之间的分钟差""" @@ -174,14 +164,127 @@ class ScheduleGenerator: def print_schedule(self): """打印完整的日程安排""" - if not self._parse_schedule(self.today_schedule_text): - logger.warning("今日日程有误,将在两小时后重新生成") + if not self.today_schedule_text: + logger.warning("今日日程有误,将在下次运行时重新生成") db.schedule.delete_one({"date": datetime.datetime.now().strftime("%Y-%m-%d")}) else: logger.info("=== 今日日程安排 ===") - for time_str, activity in self.today_schedule.items(): - logger.info(f"时间[{time_str}]: 活动[{activity}]") + logger.info(self.today_schedule_text) logger.info("==================") self.enable_output = False + + async def update_today_done_list(self): + # 更新数据库中的 today_done_list + today_str = datetime.datetime.now().strftime("%Y-%m-%d") + existing_schedule = db.schedule.find_one({"date": today_str}) + + if existing_schedule: + # 更新数据库中的 today_done_list + db.schedule.update_one( + {"date": today_str}, + {"$set": {"today_done_list": self.today_done_list}} + ) + logger.debug(f"已更新{today_str}的已完成活动列表") + else: + logger.warning(f"未找到{today_str}的日程记录") + + async def move_doing(self,mind_thinking: str = ""): + current_time = datetime.datetime.now() + doing_prompt = self.construct_doing_prompt(current_time,mind_thinking) + doing_response,_ = await self.llm_scheduler_doing.generate_response_async(doing_prompt) + self.today_done_list.append((current_time,doing_response)) + + await self.update_today_done_list() + + return doing_response + + async def get_task_from_time_to_time(self, start_time: str, end_time: str): + """获取指定时间范围内的任务列表 + + Args: + start_time (str): 开始时间,格式为"HH:MM" + end_time (str): 结束时间,格式为"HH:MM" + + Returns: + list: 时间范围内的任务列表 + """ + result = [] + for task in self.today_done_list: + task_time = task[0] # 获取任务的时间戳 + task_time_str = task_time.strftime("%H:%M") + + # 检查任务时间是否在指定范围内 + if self._time_diff(start_time, task_time_str) >= 0 and self._time_diff(task_time_str, end_time) >= 0: + result.append(task) + + return result + + def get_current_num_task(self, num=1, time_info = False): + """获取最新加入的指定数量的日程 + + Args: + num (int): 需要获取的日程数量,默认为1 + + Returns: + list: 最新加入的日程列表 + """ + if not self.today_done_list: + return [] + + # 确保num不超过列表长度 + num = min(num, len(self.today_done_list)) + pre_doings = "" + for doing in self.today_done_list[-num:]: + + if time_info: + time_str = doing[0].strftime("%H:%M") + pre_doings += time_str + "时," + doing[1] + "\n" + else: + pre_doings += doing[1] + "\n" + + # 返回最新的num条日程 + return pre_doings + + def save_today_schedule_to_db(self): + """保存日程到数据库,同时初始化 today_done_list""" + date_str = datetime.datetime.now().strftime("%Y-%m-%d") + schedule_data = { + "date": date_str, + "schedule": self.today_schedule_text, + "today_done_list": self.today_done_list if hasattr(self, 'today_done_list') else [] + } + # 使用 upsert 操作,如果存在则更新,不存在则插入 + db.schedule.update_one( + {"date": date_str}, + {"$set": schedule_data}, + upsert=True + ) + logger.debug(f"已保存{date_str}的日程到数据库") + + def load_schedule_from_db(self, date: datetime.datetime): + """从数据库加载日程,同时加载 today_done_list""" + date_str = date.strftime("%Y-%m-%d") + existing_schedule = db.schedule.find_one({"date": date_str}) + + if existing_schedule: + schedule_text = existing_schedule["schedule"] + return schedule_text, existing_schedule.get("today_done_list", []) + else: + logger.debug(f"{date_str}的日程不存在") + return None, None + +async def main(): + # 使用示例 + scheduler = ScheduleGenerator() + scheduler.initialize(name="麦麦", personality="你叫麦麦,你19岁,是一个大二的女大学生,你有一头黑色短发,你会刷贴吧,你现在在学习心理学", behavior="你比较内向,一般熬夜比较晚,然后第二天早上10点起床吃早午饭",interval=60) + await scheduler.mai_schedule_start() + + + # 当作为组件导入时使用的实例 bot_schedule = ScheduleGenerator() + +if __name__ == "__main__": + import asyncio + # 当直接运行此文件时执行 + asyncio.run(main()) diff --git a/src/plugins/schedule/schedule_generator_pro.py b/src/plugins/schedule/schedule_generator_pro.py deleted file mode 100644 index ceaf2afd2..000000000 --- a/src/plugins/schedule/schedule_generator_pro.py +++ /dev/null @@ -1,264 +0,0 @@ -import datetime -import json -import re -import os -import sys -from typing import Dict, Union -# 添加项目根目录到 Python 路径 -root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) -sys.path.append(root_path) - -from src.common.database import db # noqa: E402 -from src.common.logger import get_module_logger # noqa: E402 -from src.plugins.schedule.offline_llm import LLMModel # noqa: E402 - -logger = get_module_logger("scheduler") - - -class ScheduleGenerator: - enable_output: bool = True - - def __init__(self, name: str = "bot_name", personality: str = "你是一个爱国爱党的新时代青年", behavior: str = "你非常外向,喜欢尝试新事物和人交流"): - # 使用离线LLM模型 - self.llm_scheduler = LLMModel(model_name="Qwen/Qwen2.5-32B-Instruct", temperature=0.9) - - self.today_schedule_text = "" - self.today_done_list = [] - - self.yesterday_schedule_text = "" - self.yesterday_done_list = [] - - self.name = name - self.personality = personality - self.behavior = behavior - - self.start_time = datetime.datetime.now() - - self.schedule_doing_update_interval = 60 #最好大于60 - - async def mai_schedule_start(self): - """启动日程系统,每5分钟执行一次move_doing,并在日期变化时重新检查日程""" - try: - logger.info(f"日程系统启动/刷新时间: {self.start_time.strftime('%Y-%m-%d %H:%M:%S')}") - # 初始化日程 - await self.check_and_create_today_schedule() - self.print_schedule() - - while True: - print(self.get_current_num_task(1, True)) - - current_time = datetime.datetime.now() - - # 检查是否需要重新生成日程(日期变化) - if current_time.date() != self.start_time.date(): - logger.info("检测到日期变化,重新生成日程") - self.start_time = current_time - await self.check_and_create_today_schedule() - self.print_schedule() - - # 执行当前活动 - current_activity = await self.move_doing() - logger.info(f"当前活动: {current_activity}") - - await asyncio.sleep(self.schedule_doing_update_interval) - - except Exception as e: - logger.error(f"日程系统运行时出错: {str(e)}") - logger.exception("详细错误信息:") - - async def check_and_create_today_schedule(self): - """检查昨天的日程,并确保今天有日程安排 - - Returns: - tuple: (today_schedule_text, today_schedule) 今天的日程文本和解析后的日程字典 - """ - today = datetime.datetime.now() - yesterday = today - datetime.timedelta(days=1) - - # 先检查昨天的日程 - self.yesterday_schedule_text, self.yesterday_done_list = self.load_schedule_from_db(yesterday) - if self.yesterday_schedule_text: - logger.debug(f"已加载{yesterday.strftime('%Y-%m-%d')}的日程") - - # 检查今天的日程 - self.today_schedule_text, self.today_done_list = self.load_schedule_from_db(today) - if not self.today_done_list: - self.today_done_list = [] - if not self.today_schedule_text: - logger.info(f"{today.strftime('%Y-%m-%d')}的日程不存在,准备生成新的日程") - self.today_schedule_text = await self.generate_daily_schedule(target_date=today) - - self.save_today_schedule_to_db() - - def construct_daytime_prompt(self, target_date: datetime.datetime): - date_str = target_date.strftime("%Y-%m-%d") - weekday = target_date.strftime("%A") - - prompt = f"我是{self.name},{self.personality},{self.behavior}" - prompt += f"我昨天的日程是:{self.yesterday_schedule_text}\n" - prompt += f"请为我生成{date_str}({weekday})的日程安排,结合我的个人特点和行为习惯\n" - prompt += "推测我的日程安排,包括我一天都在做什么,有什么发现和思考,具体一些,详细一些,记得写明时间\n" - prompt += "直接返回我的日程,不要输出其他内容:" - return prompt - - def construct_doing_prompt(self,time: datetime.datetime): - now_time = time.strftime("%H:%M") - if self.today_done_list: - previous_doing = self.get_current_num_task(10, True) - print(previous_doing) - else: - previous_doing = "我没做什么事情" - - - prompt = f"我是{self.name},{self.personality},{self.behavior}" - prompt += f"我今天的日程是:{self.today_schedule_text}\n" - prompt += f"我之前做了的事情是:{previous_doing},从之前到现在已经过去了{self.schedule_doing_update_interval/60}分钟了\n" - prompt += f"现在是{now_time},结合我的个人特点和行为习惯," - prompt += "推测我现在做什么,具体一些,详细一些\n" - prompt += "直接返回我在做的事情,不要输出其他内容:" - return prompt - - async def generate_daily_schedule( - self, target_date: datetime.datetime = None,) -> Dict[str, str]: - daytime_prompt = self.construct_daytime_prompt(target_date) - daytime_response,_ = await self.llm_scheduler.generate_response_async(daytime_prompt) - return daytime_response - - def _time_diff(self, time1: str, time2: str) -> int: - """计算两个时间字符串之间的分钟差""" - if time1 == "24:00": - time1 = "23:59" - if time2 == "24:00": - time2 = "23:59" - t1 = datetime.datetime.strptime(time1, "%H:%M") - t2 = datetime.datetime.strptime(time2, "%H:%M") - diff = int((t2 - t1).total_seconds() / 60) - # 考虑时间的循环性 - if diff < -720: - diff += 1440 # 加一天的分钟 - elif diff > 720: - diff -= 1440 # 减一天的分钟 - # print(f"时间1[{time1}]: 时间2[{time2}],差值[{diff}]分钟") - return diff - - def print_schedule(self): - """打印完整的日程安排""" - if not self.today_schedule_text: - logger.warning("今日日程有误,将在下次运行时重新生成") - db.schedule.delete_one({"date": datetime.datetime.now().strftime("%Y-%m-%d")}) - else: - logger.info("=== 今日日程安排 ===") - logger.info(self.today_schedule_text) - logger.info("==================") - self.enable_output = False - - async def update_today_done_list(self): - # 更新数据库中的 today_done_list - today_str = datetime.datetime.now().strftime("%Y-%m-%d") - existing_schedule = db.schedule.find_one({"date": today_str}) - - if existing_schedule: - # 更新数据库中的 today_done_list - db.schedule.update_one( - {"date": today_str}, - {"$set": {"today_done_list": self.today_done_list}} - ) - logger.debug(f"已更新{today_str}的已完成活动列表") - else: - logger.warning(f"未找到{today_str}的日程记录") - - async def move_doing(self): - current_time = datetime.datetime.now() - time_str = current_time.strftime("%H:%M") - doing_prompt = self.construct_doing_prompt(current_time) - doing_response,_ = await self.llm_scheduler.generate_response_async(doing_prompt) - self.today_done_list.append((current_time, time_str + "时," + doing_response)) - - await self.update_today_done_list() - - return doing_response - - async def get_task_from_time_to_time(self, start_time: str, end_time: str): - """获取指定时间范围内的任务列表 - - Args: - start_time (str): 开始时间,格式为"HH:MM" - end_time (str): 结束时间,格式为"HH:MM" - - Returns: - list: 时间范围内的任务列表 - """ - result = [] - for task in self.today_done_list: - task_time = task[0] # 获取任务的时间戳 - task_time_str = task_time.strftime("%H:%M") - - # 检查任务时间是否在指定范围内 - if self._time_diff(start_time, task_time_str) >= 0 and self._time_diff(task_time_str, end_time) >= 0: - result.append(task) - - return result - - def get_current_num_task(self, num=1, time_info = False): - """获取最新加入的指定数量的日程 - - Args: - num (int): 需要获取的日程数量,默认为1 - - Returns: - list: 最新加入的日程列表 - """ - if not self.today_done_list: - return [] - - # 确保num不超过列表长度 - num = min(num, len(self.today_done_list)) - pre_doing = "" - for doing in self.today_done_list[-num:]: - pre_doing += doing[1] - - # 返回最新的num条日程 - return pre_doing - - def save_today_schedule_to_db(self): - """保存日程到数据库,同时初始化 today_done_list""" - date_str = datetime.datetime.now().strftime("%Y-%m-%d") - schedule_data = { - "date": date_str, - "schedule": self.today_schedule_text, - "today_done_list": self.today_done_list if hasattr(self, 'today_done_list') else [] - } - # 使用 upsert 操作,如果存在则更新,不存在则插入 - db.schedule.update_one( - {"date": date_str}, - {"$set": schedule_data}, - upsert=True - ) - logger.debug(f"已保存{date_str}的日程到数据库") - - def load_schedule_from_db(self, date: datetime.datetime): - """从数据库加载日程,同时加载 today_done_list""" - date_str = date.strftime("%Y-%m-%d") - existing_schedule = db.schedule.find_one({"date": date_str}) - - if existing_schedule: - schedule_text = existing_schedule["schedule"] - return schedule_text, existing_schedule.get("today_done_list", []) - else: - logger.debug(f"{date_str}的日程不存在") - return None, None - -async def main(): - # 使用示例 - scheduler = ScheduleGenerator(name="麦麦", personality="你叫麦麦,你19岁,是一个大二的女大学生,你有一头黑色短发,你会刷贴吧,你现在在学习心理学", behavior="你比较内向,一般熬夜比较晚,然后第二天早上10点起床吃早午饭") - await scheduler.mai_schedule_start() - - - -# 当作为组件导入时使用的实例 -bot_schedule = ScheduleGenerator() - -if __name__ == "__main__": - import asyncio - # 当直接运行此文件时执行 - asyncio.run(main()) diff --git a/src/think_flow_demo/current_mind.py b/src/think_flow_demo/current_mind.py index 6facdbf9b..cce93dc75 100644 --- a/src/think_flow_demo/current_mind.py +++ b/src/think_flow_demo/current_mind.py @@ -5,6 +5,8 @@ from src.plugins.models.utils_model import LLM_request from src.plugins.chat.config import global_config import re import time +from src.plugins.schedule.schedule_generator import bot_schedule + class CuttentState: def __init__(self): self.willing = 0 @@ -57,10 +59,12 @@ class SubHeartflow: personality_info = open("src/think_flow_demo/personality_info.txt", "r", encoding="utf-8").read() current_thinking_info = self.current_mind mood_info = self.current_state.mood - related_memory_info = 'memory' + related_memory_info = '' message_stream_info = self.outer_world.talking_summary + schedule_info = bot_schedule.get_current_num_task(num = 2,time_info = False) prompt = "" + prompt += f"你刚刚在做的事情是:{schedule_info}\n" # prompt += f"麦麦的总体想法是:{self.main_heartflow_info}\n\n" prompt += f"{personality_info}\n" prompt += f"现在你正在上网,和qq群里的网友们聊天,群里正在聊的话题是:{message_stream_info}\n" diff --git a/src/think_flow_demo/heartflow.py b/src/think_flow_demo/heartflow.py index 45843e490..a0a2d4c16 100644 --- a/src/think_flow_demo/heartflow.py +++ b/src/think_flow_demo/heartflow.py @@ -2,6 +2,7 @@ from .current_mind import SubHeartflow from src.plugins.moods.moods import MoodManager from src.plugins.models.utils_model import LLM_request from src.plugins.chat.config import global_config +from src.plugins.schedule.schedule_generator import bot_schedule import asyncio class CuttentState: @@ -30,8 +31,8 @@ class Heartflow: async def heartflow_start_working(self): while True: - # await self.do_a_thinking() - await asyncio.sleep(60) + await self.do_a_thinking() + await asyncio.sleep(900) async def do_a_thinking(self): print("麦麦大脑袋转起来了") @@ -43,9 +44,11 @@ class Heartflow: related_memory_info = 'memory' sub_flows_info = await self.get_all_subheartflows_minds() + schedule_info = bot_schedule.get_current_num_task(num = 5,time_info = True) + prompt = "" + prompt += f"你刚刚在做的事情是:{schedule_info}\n" prompt += f"{personality_info}\n" - # prompt += f"现在你正在上网,和qq群里的网友们聊天,群里正在聊的话题是:{message_stream_info}\n" prompt += f"你想起来{related_memory_info}。" prompt += f"刚刚你的主要想法是{current_thinking_info}。" prompt += f"你还有一些小想法,因为你在参加不同的群聊天,是你正在做的事情:{sub_flows_info}\n" diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 6591d4272..668b40b8e 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -43,6 +43,7 @@ personality_3_probability = 0.1 # 第三种人格出现概率,请确保三个 [schedule] enable_schedule_gen = true # 是否启用日程表(尚未完成) prompt_schedule_gen = "用几句话描述描述性格特点或行动规律,这个特征会用来生成日程表" +schedule_doing_update_interval = 900 # 日程表更新间隔 单位秒 [message] max_context_size = 15 # 麦麦获得的上文数量,建议15,太短太长都会导致脑袋尖尖 From 67291f1b4906b399f7acc22fe7c0387557b0688b Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 26 Mar 2025 23:19:19 +0800 Subject: [PATCH 23/46] =?UTF-8?q?better:=E4=B8=8D=E5=A5=BD=E6=84=8F?= =?UTF-8?q?=E6=80=9D=E5=88=9A=E5=88=9A=E4=B8=8D=E8=A1=8C=EF=BC=8C=E7=8E=B0?= =?UTF-8?q?=E5=9C=A8=E5=8F=AF=E4=BB=A5=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/logger.py | 18 ++++++++++++++++++ src/plugins/schedule/schedule_generator.py | 14 +++++++++++--- src/think_flow_demo/heartflow.py | 17 ++++++++++++++--- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/common/logger.py b/src/common/logger.py index b910427bf..8a9d08926 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -122,6 +122,23 @@ SENDER_STYLE_CONFIG = { }, } +HEARTFLOW_STYLE_CONFIG = { + "advanced": { + "console_format": ( + "{time:YYYY-MM-DD HH:mm:ss} | " + "{level: <8} | " + "{extra[module]: <12} | " + "麦麦大脑袋 | " + "{message}" + ), + "file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦大脑袋 | {message}"), + }, + "simple": { + "console_format": ("{time:MM-DD HH:mm} | 麦麦大脑袋 | {message}"), # noqa: E501 + "file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦大脑袋 | {message}"), + }, +} + SCHEDULE_STYLE_CONFIG = { "advanced": { "console_format": ( @@ -201,6 +218,7 @@ LLM_STYLE_CONFIG = LLM_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else LLM_STYLE_CO CHAT_STYLE_CONFIG = CHAT_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else CHAT_STYLE_CONFIG["advanced"] MOOD_STYLE_CONFIG = MOOD_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else MOOD_STYLE_CONFIG["advanced"] SCHEDULE_STYLE_CONFIG = SCHEDULE_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else SCHEDULE_STYLE_CONFIG["advanced"] +HEARTFLOW_STYLE_CONFIG = HEARTFLOW_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else HEARTFLOW_STYLE_CONFIG["advanced"] def is_registered_module(record: dict) -> bool: """检查是否为已注册的模块""" diff --git a/src/plugins/schedule/schedule_generator.py b/src/plugins/schedule/schedule_generator.py index d39b0517d..a02a22352 100644 --- a/src/plugins/schedule/schedule_generator.py +++ b/src/plugins/schedule/schedule_generator.py @@ -76,8 +76,9 @@ class ScheduleGenerator: self.print_schedule() # 执行当前活动 - current_activity = await self.move_doing(mind_thinking="") - logger.info(f"当前活动: {current_activity}") + # mind_thinking = subheartflow_manager.current_state.current_mind + + await self.move_doing() await asyncio.sleep(self.schedule_doing_update_interval) @@ -190,12 +191,19 @@ class ScheduleGenerator: async def move_doing(self,mind_thinking: str = ""): current_time = datetime.datetime.now() - doing_prompt = self.construct_doing_prompt(current_time,mind_thinking) + if mind_thinking: + doing_prompt = self.construct_doing_prompt(current_time,mind_thinking) + else: + doing_prompt = self.construct_doing_prompt(current_time) + + print(doing_prompt) doing_response,_ = await self.llm_scheduler_doing.generate_response_async(doing_prompt) self.today_done_list.append((current_time,doing_response)) await self.update_today_done_list() + logger.info(f"当前活动: {doing_response}") + return doing_response async def get_task_from_time_to_time(self, start_time: str, end_time: str): diff --git a/src/think_flow_demo/heartflow.py b/src/think_flow_demo/heartflow.py index a0a2d4c16..b80bb0885 100644 --- a/src/think_flow_demo/heartflow.py +++ b/src/think_flow_demo/heartflow.py @@ -4,6 +4,14 @@ from src.plugins.models.utils_model import LLM_request from src.plugins.chat.config import global_config from src.plugins.schedule.schedule_generator import bot_schedule import asyncio +from src.common.logger import get_module_logger, LogConfig, HEARTFLOW_STYLE_CONFIG # noqa: E402 + +heartflow_config = LogConfig( + # 使用海马体专用样式 + console_format=HEARTFLOW_STYLE_CONFIG["console_format"], + file_format=HEARTFLOW_STYLE_CONFIG["file_format"], +) +logger = get_module_logger("heartflow", config=heartflow_config) class CuttentState: def __init__(self): @@ -32,10 +40,10 @@ class Heartflow: async def heartflow_start_working(self): while True: await self.do_a_thinking() - await asyncio.sleep(900) + await asyncio.sleep(100) async def do_a_thinking(self): - print("麦麦大脑袋转起来了") + logger.info("麦麦大脑袋转起来了") self.current_state.update_current_state_info() personality_info = open("src/think_flow_demo/personality_info.txt", "r", encoding="utf-8").read() @@ -61,7 +69,10 @@ class Heartflow: self.update_current_mind(reponse) self.current_mind = reponse - print(f"麦麦的总体脑内状态:{self.current_mind}") + logger.info(f"麦麦的总体脑内状态:{self.current_mind}") + logger.info("麦麦想了想,当前活动:") + await bot_schedule.move_doing(self.current_mind) + for _, subheartflow in self._subheartflows.items(): subheartflow.main_heartflow_info = reponse From 805bde06462f19c57003739efc71e7886508b856 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 26 Mar 2025 23:26:32 +0800 Subject: [PATCH 24/46] Update schedule_generator.py --- src/plugins/schedule/schedule_generator.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/schedule/schedule_generator.py b/src/plugins/schedule/schedule_generator.py index a02a22352..f2bce21ce 100644 --- a/src/plugins/schedule/schedule_generator.py +++ b/src/plugins/schedule/schedule_generator.py @@ -117,14 +117,14 @@ class ScheduleGenerator: prompt = f"你是{self.name},{self.personality},{self.behavior}" prompt += f"你昨天的日程是:{self.yesterday_schedule_text}\n" prompt += f"请为你生成{date_str}({weekday})的日程安排,结合你的个人特点和行为习惯\n" - prompt += "推测你的日程安排,包括你一天都在做什么,有什么发现和思考,具体一些,详细一些,需要1500字以上,精确到每半个小时,记得写明时间\n" - prompt += "直接返回你的日程,不要输出其他内容:" + prompt += "推测你的日程安排,包括你一天都在做什么,从起床到睡眠,有什么发现和思考,具体一些,详细一些,需要1500字以上,精确到每半个小时,记得写明时间\n" + prompt += "直接返回你的日程,从起床到睡觉,不要输出其他内容:" return prompt def construct_doing_prompt(self,time: datetime.datetime,mind_thinking: str = ""): now_time = time.strftime("%H:%M") if self.today_done_list: - previous_doings = self.get_current_num_task(10, True) + previous_doings = self.get_current_num_task(5, True) # print(previous_doings) else: previous_doings = "你没做什么事情" @@ -132,9 +132,9 @@ class ScheduleGenerator: prompt = f"你是{self.name},{self.personality},{self.behavior}" prompt += f"你今天的日程是:{self.today_schedule_text}\n" + prompt += f"你之前做了的事情是:{previous_doings},从之前到现在已经过去了{self.schedule_doing_update_interval/60}分钟了\n" if mind_thinking: prompt += f"你脑子里在想:{mind_thinking}\n" - prompt += f"你之前做了的事情是:{previous_doings},从之前到现在已经过去了{self.schedule_doing_update_interval/60}分钟了\n" prompt += f"现在是{now_time},结合你的个人特点和行为习惯," prompt += "推测你现在做什么,具体一些,详细一些\n" prompt += "直接返回你在做的事情,不要输出其他内容:" @@ -196,7 +196,7 @@ class ScheduleGenerator: else: doing_prompt = self.construct_doing_prompt(current_time) - print(doing_prompt) + # print(doing_prompt) doing_response,_ = await self.llm_scheduler_doing.generate_response_async(doing_prompt) self.today_done_list.append((current_time,doing_response)) From def7ee7ace9e3030e0cbc3c67d7edee529f3e3fd Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 26 Mar 2025 23:38:37 +0800 Subject: [PATCH 25/46] Update message_sender.py --- src/plugins/chat/message_sender.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index 8a9b44467..7528a2e5a 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -61,6 +61,7 @@ class Message_Sender: if not is_recalled: typing_time = calculate_typing_time(message.processed_plain_text) + logger.info(f"麦麦正在打字,预计需要{typing_time}秒") await asyncio.sleep(typing_time) message_json = message.to_dict() @@ -99,7 +100,7 @@ class MessageContainer: self.max_size = max_size self.messages = [] self.last_send_time = 0 - self.thinking_timeout = 20 # 思考超时时间(秒) + self.thinking_timeout = 10 # 思考超时时间(秒) def get_timeout_messages(self) -> List[MessageSending]: """获取所有超时的Message_Sending对象(思考时间超过30秒),按thinking_start_time排序""" @@ -208,7 +209,7 @@ class MessageManager: # print(thinking_time) if ( message_earliest.is_head - and message_earliest.update_thinking_time() > 15 + and message_earliest.update_thinking_time() > 20 and not message_earliest.is_private_message() # 避免在私聊时插入reply ): logger.debug(f"设置回复消息{message_earliest.processed_plain_text}") @@ -235,7 +236,7 @@ class MessageManager: # print(msg.is_private_message()) if ( msg.is_head - and msg.update_thinking_time() > 15 + and msg.update_thinking_time() > 25 and not msg.is_private_message() # 避免在私聊时插入reply ): logger.debug(f"设置回复消息{msg.processed_plain_text}") From 5886b1c849c8080ac29d14ef739cb1ee65e2f3fc Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 27 Mar 2025 00:10:21 +0800 Subject: [PATCH 26/46] =?UTF-8?q?fix=20=E4=BF=AE=E5=A4=8D=E4=BA=86?= =?UTF-8?q?=E7=82=B9=E5=B0=8Fbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/schedule/schedule_generator.py | 4 +-- src/plugins/utils/statistic.py | 37 +++++++++++++++++++--- src/think_flow_demo/current_mind.py | 4 +-- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/plugins/schedule/schedule_generator.py b/src/plugins/schedule/schedule_generator.py index f2bce21ce..b14ebd1ba 100644 --- a/src/plugins/schedule/schedule_generator.py +++ b/src/plugins/schedule/schedule_generator.py @@ -27,7 +27,7 @@ class ScheduleGenerator: def __init__(self, ): # 使用离线LLM模型 self.llm_scheduler_all = LLM_request( - model= global_config.llm_reasoning, temperature=0.9, max_tokens=2048,request_type="schedule") + model= global_config.llm_reasoning, temperature=0.9, max_tokens=7000,request_type="schedule") self.llm_scheduler_doing = LLM_request( model= global_config.llm_normal, temperature=0.9, max_tokens=2048,request_type="schedule") @@ -43,7 +43,7 @@ class ScheduleGenerator: self.start_time = datetime.datetime.now() - self.schedule_doing_update_interval = 60 #最好大于60 + self.schedule_doing_update_interval = 300 #最好大于60 def initialize(self,name: str = "bot_name", personality: str = "你是一个爱国爱党的新时代青年", behavior: str = "你非常外向,喜欢尝试新事物和人交流",interval: int = 60): """初始化日程系统""" diff --git a/src/plugins/utils/statistic.py b/src/plugins/utils/statistic.py index f03067cb1..aad33e88c 100644 --- a/src/plugins/utils/statistic.py +++ b/src/plugins/utils/statistic.py @@ -20,6 +20,13 @@ class LLMStatistics: self.output_file = output_file self.running = False self.stats_thread = None + self._init_database() + + def _init_database(self): + """初始化数据库集合""" + if "online_time" not in db.list_collection_names(): + db.create_collection("online_time") + db.online_time.create_index([("timestamp", 1)]) def start(self): """启动统计线程""" @@ -35,6 +42,16 @@ class LLMStatistics: if self.stats_thread: self.stats_thread.join() + def _record_online_time(self): + """记录在线时间""" + try: + db.online_time.insert_one({ + "timestamp": datetime.now(), + "duration": 5 # 5分钟 + }) + except Exception: + logger.exception("记录在线时间失败") + def _collect_statistics_for_period(self, start_time: datetime) -> Dict[str, Any]: """收集指定时间段的LLM请求统计数据 @@ -56,10 +73,11 @@ class LLMStatistics: "tokens_by_type": defaultdict(int), "tokens_by_user": defaultdict(int), "tokens_by_model": defaultdict(int), + # 新增在线时间统计 + "online_time_minutes": 0, } cursor = db.llm_usage.find({"timestamp": {"$gte": start_time}}) - total_requests = 0 for doc in cursor: @@ -74,7 +92,7 @@ class LLMStatistics: prompt_tokens = doc.get("prompt_tokens", 0) completion_tokens = doc.get("completion_tokens", 0) - total_tokens = prompt_tokens + completion_tokens # 根据数据库字段调整 + total_tokens = prompt_tokens + completion_tokens stats["tokens_by_type"][request_type] += total_tokens stats["tokens_by_user"][user_id] += total_tokens stats["tokens_by_model"][model_name] += total_tokens @@ -91,6 +109,11 @@ class LLMStatistics: if total_requests > 0: stats["average_tokens"] = stats["total_tokens"] / total_requests + # 统计在线时间 + online_time_cursor = db.online_time.find({"timestamp": {"$gte": start_time}}) + for doc in online_time_cursor: + stats["online_time_minutes"] += doc.get("duration", 0) + return stats def _collect_all_statistics(self) -> Dict[str, Dict[str, Any]]: @@ -115,7 +138,8 @@ class LLMStatistics: output.append(f"总请求数: {stats['total_requests']}") if stats["total_requests"] > 0: output.append(f"总Token数: {stats['total_tokens']}") - output.append(f"总花费: {stats['total_cost']:.4f}¥\n") + output.append(f"总花费: {stats['total_cost']:.4f}¥") + output.append(f"在线时间: {stats['online_time_minutes']}分钟\n") data_fmt = "{:<32} {:>10} {:>14} {:>13.4f} ¥" @@ -184,13 +208,16 @@ class LLMStatistics: """统计循环,每1分钟运行一次""" while self.running: try: + # 记录在线时间 + self._record_online_time() + # 收集并保存统计数据 all_stats = self._collect_all_statistics() self._save_statistics(all_stats) except Exception: logger.exception("统计数据处理失败") - # 等待1分钟 - for _ in range(60): + # 等待5分钟 + for _ in range(300): # 5分钟 = 300秒 if not self.running: break time.sleep(1) diff --git a/src/think_flow_demo/current_mind.py b/src/think_flow_demo/current_mind.py index cce93dc75..f15b036c3 100644 --- a/src/think_flow_demo/current_mind.py +++ b/src/think_flow_demo/current_mind.py @@ -46,11 +46,11 @@ class SubHeartflow: current_time = time.time() if current_time - self.last_reply_time > 180: # 3分钟 = 180秒 # print(f"{self.observe_chat_id}麦麦已经3分钟没有回复了,暂时停止思考") - await asyncio.sleep(25) # 每30秒检查一次 + await asyncio.sleep(60) # 每30秒检查一次 else: await self.do_a_thinking() await self.judge_willing() - await asyncio.sleep(25) + await asyncio.sleep(60) async def do_a_thinking(self): print("麦麦小脑袋转起来了") From 8da4729c175bbe4ff5fe35ba9de63d68b142b9a6 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 27 Mar 2025 00:23:13 +0800 Subject: [PATCH 27/46] fix ruff --- src/plugins/chat/__init__.py | 6 ++- src/plugins/chat/prompt_builder.py | 2 +- src/plugins/schedule/offline_llm.py | 3 -- src/plugins/schedule/schedule_generator.py | 18 ++++++--- 配置文件错误排查.py | 44 +++++++++++++++------- 5 files changed, 49 insertions(+), 24 deletions(-) diff --git a/src/plugins/chat/__init__.py b/src/plugins/chat/__init__.py index 8bbb16bf5..55b83e889 100644 --- a/src/plugins/chat/__init__.py +++ b/src/plugins/chat/__init__.py @@ -84,7 +84,11 @@ async def start_background_tasks(): @driver.on_startup async def init_schedule(): """在 NoneBot2 启动时初始化日程系统""" - bot_schedule.initialize(name=global_config.BOT_NICKNAME, personality=global_config.PROMPT_PERSONALITY, behavior=global_config.PROMPT_SCHEDULE_GEN, interval=global_config.SCHEDULE_DOING_UPDATE_INTERVAL) + bot_schedule.initialize( + name=global_config.BOT_NICKNAME, + personality=global_config.PROMPT_PERSONALITY, + behavior=global_config.PROMPT_SCHEDULE_GEN, + interval=global_config.SCHEDULE_DOING_UPDATE_INTERVAL) asyncio.create_task(bot_schedule.mai_schedule_start()) @driver.on_startup diff --git a/src/plugins/chat/prompt_builder.py b/src/plugins/chat/prompt_builder.py index 283ea0eae..dc2e5930e 100644 --- a/src/plugins/chat/prompt_builder.py +++ b/src/plugins/chat/prompt_builder.py @@ -57,7 +57,7 @@ class PromptBuilder: mood_prompt = mood_manager.get_prompt() # 日程构建 - schedule_prompt = f'''你现在正在做的事情是:{bot_schedule.get_current_num_task(num = 1,time_info = False)}''' + # schedule_prompt = f'''你现在正在做的事情是:{bot_schedule.get_current_num_task(num = 1,time_info = False)}''' # 获取聊天上下文 chat_in_group = True diff --git a/src/plugins/schedule/offline_llm.py b/src/plugins/schedule/offline_llm.py index f274740fa..5276f3802 100644 --- a/src/plugins/schedule/offline_llm.py +++ b/src/plugins/schedule/offline_llm.py @@ -1,10 +1,7 @@ import asyncio import os -import time -from typing import Tuple, Union import aiohttp -import requests from src.common.logger import get_module_logger logger = get_module_logger("offline_llm") diff --git a/src/plugins/schedule/schedule_generator.py b/src/plugins/schedule/schedule_generator.py index b14ebd1ba..41cf187e1 100644 --- a/src/plugins/schedule/schedule_generator.py +++ b/src/plugins/schedule/schedule_generator.py @@ -24,7 +24,7 @@ logger = get_module_logger("scheduler", config=schedule_config) class ScheduleGenerator: # enable_output: bool = True - def __init__(self, ): + def __init__(self): # 使用离线LLM模型 self.llm_scheduler_all = LLM_request( model= global_config.llm_reasoning, temperature=0.9, max_tokens=7000,request_type="schedule") @@ -45,7 +45,11 @@ class ScheduleGenerator: self.schedule_doing_update_interval = 300 #最好大于60 - def initialize(self,name: str = "bot_name", personality: str = "你是一个爱国爱党的新时代青年", behavior: str = "你非常外向,喜欢尝试新事物和人交流",interval: int = 60): + def initialize( + self,name: str = "bot_name", + personality: str = "你是一个爱国爱党的新时代青年", + behavior: str = "你非常外向,喜欢尝试新事物和人交流", + interval: int = 60): """初始化日程系统""" self.name = name self.behavior = behavior @@ -117,7 +121,7 @@ class ScheduleGenerator: prompt = f"你是{self.name},{self.personality},{self.behavior}" prompt += f"你昨天的日程是:{self.yesterday_schedule_text}\n" prompt += f"请为你生成{date_str}({weekday})的日程安排,结合你的个人特点和行为习惯\n" - prompt += "推测你的日程安排,包括你一天都在做什么,从起床到睡眠,有什么发现和思考,具体一些,详细一些,需要1500字以上,精确到每半个小时,记得写明时间\n" + prompt += "推测你的日程安排,包括你一天都在做什么,从起床到睡眠,有什么发现和思考,具体一些,详细一些,需要1500字以上,精确到每半个小时,记得写明时间\n" #noqa: E501 prompt += "直接返回你的日程,从起床到睡觉,不要输出其他内容:" return prompt @@ -132,7 +136,7 @@ class ScheduleGenerator: prompt = f"你是{self.name},{self.personality},{self.behavior}" prompt += f"你今天的日程是:{self.today_schedule_text}\n" - prompt += f"你之前做了的事情是:{previous_doings},从之前到现在已经过去了{self.schedule_doing_update_interval/60}分钟了\n" + prompt += f"你之前做了的事情是:{previous_doings},从之前到现在已经过去了{self.schedule_doing_update_interval/60}分钟了\n" #noqa: E501 if mind_thinking: prompt += f"你脑子里在想:{mind_thinking}\n" prompt += f"现在是{now_time},结合你的个人特点和行为习惯," @@ -284,7 +288,11 @@ class ScheduleGenerator: async def main(): # 使用示例 scheduler = ScheduleGenerator() - scheduler.initialize(name="麦麦", personality="你叫麦麦,你19岁,是一个大二的女大学生,你有一头黑色短发,你会刷贴吧,你现在在学习心理学", behavior="你比较内向,一般熬夜比较晚,然后第二天早上10点起床吃早午饭",interval=60) + scheduler.initialize( + name="麦麦", + personality="你叫麦麦,你19岁,是一个大二的女大学生,你有一头黑色短发,你会刷贴吧,你现在在学习心理学", + behavior="你比较内向,一般熬夜比较晚,然后第二天早上10点起床吃早午饭", + interval=60) await scheduler.mai_schedule_start() diff --git a/配置文件错误排查.py b/配置文件错误排查.py index 114171135..d277ceb4a 100644 --- a/配置文件错误排查.py +++ b/配置文件错误排查.py @@ -1,8 +1,7 @@ import tomli import sys -import re from pathlib import Path -from typing import Dict, Any, List, Set, Tuple +from typing import Dict, Any, List, Tuple def load_toml_file(file_path: str) -> Dict[str, Any]: """加载TOML文件""" @@ -184,10 +183,15 @@ def check_model_configurations(config: Dict[str, Any], env_vars: Dict[str, str]) provider = model_config["provider"].upper() # 检查拼写错误 - for known_provider, correct_provider in reverse_mapping.items(): + for known_provider, _correct_provider in reverse_mapping.items(): # 使用模糊匹配检测拼写错误 - if provider != known_provider and _similar_strings(provider, known_provider) and provider not in reverse_mapping: - errors.append(f"[model.{model_name}]的provider '{model_config['provider']}' 可能拼写错误,应为 '{known_provider}'") + if (provider != known_provider and + _similar_strings(provider, known_provider) and + provider not in reverse_mapping): + errors.append( + f"[model.{model_name}]的provider '{model_config['provider']}' " + f"可能拼写错误,应为 '{known_provider}'" + ) break return errors @@ -223,7 +227,7 @@ def check_api_providers(config: Dict[str, Any], env_vars: Dict[str, str]) -> Lis # 检查配置文件中使用的所有提供商 used_providers = set() - for model_category, model_config in config["model"].items(): + for _model_category, model_config in config["model"].items(): if "provider" in model_config: provider = model_config["provider"] used_providers.add(provider) @@ -247,7 +251,7 @@ def check_api_providers(config: Dict[str, Any], env_vars: Dict[str, str]) -> Lis # 特别检查常见的拼写错误 for provider in used_providers: if provider.upper() == "SILICONFOLW": - errors.append(f"提供商 'SILICONFOLW' 存在拼写错误,应为 'SILICONFLOW'") + errors.append("提供商 'SILICONFOLW' 存在拼写错误,应为 'SILICONFLOW'") return errors @@ -272,7 +276,7 @@ def check_groups_configuration(config: Dict[str, Any]) -> List[str]: "main": "groups.talk_allowed中存在默认示例值'123',请修改为真实的群号", "details": [ f" 当前值: {groups['talk_allowed']}", - f" '123'为示例值,需要替换为真实群号" + " '123'为示例值,需要替换为真实群号" ] }) @@ -371,7 +375,8 @@ def check_memory_config(config: Dict[str, Any]) -> List[str]: if "memory_compress_rate" in memory and (memory["memory_compress_rate"] <= 0 or memory["memory_compress_rate"] > 1): errors.append(f"memory.memory_compress_rate值无效: {memory['memory_compress_rate']}, 应在0-1之间") - if "memory_forget_percentage" in memory and (memory["memory_forget_percentage"] <= 0 or memory["memory_forget_percentage"] > 1): + if ("memory_forget_percentage" in memory + and (memory["memory_forget_percentage"] <= 0 or memory["memory_forget_percentage"] > 1)): errors.append(f"memory.memory_forget_percentage值无效: {memory['memory_forget_percentage']}, 应在0-1之间") return errors @@ -393,7 +398,10 @@ def check_personality_config(config: Dict[str, Any]) -> List[str]: else: # 检查数组长度 if len(personality["prompt_personality"]) < 1: - errors.append(f"personality.prompt_personality数组长度不足,当前长度: {len(personality['prompt_personality'])}, 需要至少1项") + errors.append( + f"personality.prompt_personality至少需要1项," + f"当前长度: {len(personality['prompt_personality'])}" + ) else: # 模板默认值 template_values = [ @@ -452,10 +460,13 @@ def check_bot_config(config: Dict[str, Any]) -> List[str]: def format_results(all_errors): """格式化检查结果""" - sections_errors, prob_sum_errors, prob_range_errors, model_errors, api_errors, groups_errors, kr_errors, willing_errors, memory_errors, personality_errors, bot_results = all_errors + sections_errors, prob_sum_errors, prob_range_errors, model_errors, api_errors, groups_errors, kr_errors, willing_errors, memory_errors, personality_errors, bot_results = all_errors # noqa: E501, F821 bot_errors, bot_infos = bot_results - if not any([sections_errors, prob_sum_errors, prob_range_errors, model_errors, api_errors, groups_errors, kr_errors, willing_errors, memory_errors, personality_errors, bot_errors]): + if not any([ + sections_errors, prob_sum_errors, + prob_range_errors, model_errors, api_errors, groups_errors, + kr_errors, willing_errors, memory_errors, personality_errors, bot_errors]): result = "✅ 配置文件检查通过,未发现问题。" # 添加机器人信息 @@ -574,7 +585,10 @@ def main(): bot_results = check_bot_config(config) # 格式化并打印结果 - all_errors = (sections_errors, prob_sum_errors, prob_range_errors, model_errors, api_errors, groups_errors, kr_errors, willing_errors, memory_errors, personality_errors, bot_results) + all_errors = ( + sections_errors, prob_sum_errors, + prob_range_errors, model_errors, api_errors, groups_errors, + kr_errors, willing_errors, memory_errors, personality_errors, bot_results) result = format_results(all_errors) print("📋 机器人配置检查结果:") print(result) @@ -586,7 +600,9 @@ def main(): bot_errors, _ = bot_results # 计算普通错误列表的长度 - for errors in [sections_errors, model_errors, api_errors, groups_errors, kr_errors, willing_errors, memory_errors, bot_errors]: + for errors in [ + sections_errors, model_errors, api_errors, + groups_errors, kr_errors, willing_errors, memory_errors, bot_errors]: total_errors += len(errors) # 计算元组列表的长度(概率相关错误) From c53ad9e38cafaee532ac6fc235913d97b8bb78ed Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 27 Mar 2025 00:27:25 +0800 Subject: [PATCH 28/46] Update heartflow.py --- src/think_flow_demo/heartflow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/think_flow_demo/heartflow.py b/src/think_flow_demo/heartflow.py index b80bb0885..1079483de 100644 --- a/src/think_flow_demo/heartflow.py +++ b/src/think_flow_demo/heartflow.py @@ -40,7 +40,7 @@ class Heartflow: async def heartflow_start_working(self): while True: await self.do_a_thinking() - await asyncio.sleep(100) + await asyncio.sleep(600) async def do_a_thinking(self): logger.info("麦麦大脑袋转起来了") From f0bf6fe83f9a65880cc251a0312d44b06c660b90 Mon Sep 17 00:00:00 2001 From: FuyukiVila <1642421711@qq.com> Date: Thu, 27 Mar 2025 01:04:07 +0800 Subject: [PATCH 29/46] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=BA=86prompt?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E6=98=B5=E7=A7=B0=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/think_flow_demo/heartflow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/think_flow_demo/heartflow.py b/src/think_flow_demo/heartflow.py index 1079483de..a77e8485b 100644 --- a/src/think_flow_demo/heartflow.py +++ b/src/think_flow_demo/heartflow.py @@ -97,7 +97,7 @@ class Heartflow: prompt = "" prompt += f"{personality_info}\n" prompt += f"现在{global_config.BOT_NICKNAME}的想法是:{self.current_mind}\n" - prompt += f"现在麦麦在qq群里进行聊天,聊天的话题如下:{minds_str}\n" + prompt += f"现在{global_config.BOT_NICKNAME}在qq群里进行聊天,聊天的话题如下:{minds_str}\n" prompt += f"你现在{mood_info}\n" prompt += '''现在请你总结这些聊天内容,注意关注聊天内容对原有的想法的影响,输出连贯的内心独白 不要太长,但是记得结合上述的消息,要记得你的人设,关注新内容:''' From a3811675cbfa28bef42ca2374150643ac838b7a9 Mon Sep 17 00:00:00 2001 From: AL76 <735756072@qq.com> Date: Thu, 27 Mar 2025 01:50:31 +0800 Subject: [PATCH 30/46] =?UTF-8?q?fix:=20=E5=90=8C=E6=AD=A5=E5=BF=83?= =?UTF-8?q?=E6=B5=81=E4=B8=8E=E6=9C=AC=E4=BD=93=E4=BD=BF=E7=94=A8=E7=9A=84?= =?UTF-8?q?prompt=EF=BC=8C=E4=BF=9D=E6=8C=81=E4=BA=BA=E8=AE=BE=E4=B8=80?= =?UTF-8?q?=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/think_flow_demo/current_mind.py | 13 ++++++------- src/think_flow_demo/personality_info.txt | 2 ++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/think_flow_demo/current_mind.py b/src/think_flow_demo/current_mind.py index f15b036c3..32d77ef7a 100644 --- a/src/think_flow_demo/current_mind.py +++ b/src/think_flow_demo/current_mind.py @@ -2,7 +2,7 @@ from .outer_world import outer_world import asyncio from src.plugins.moods.moods import MoodManager from src.plugins.models.utils_model import LLM_request -from src.plugins.chat.config import global_config +from src.plugins.chat.config import global_config, BotConfig import re import time from src.plugins.schedule.schedule_generator import bot_schedule @@ -36,6 +36,8 @@ class SubHeartflow: if not self.current_mind: self.current_mind = "你什么也没想" + + self.personality_info = " ".join(BotConfig.PROMPT_PERSONALITY) def assign_observe(self,stream_id): self.outer_world = outer_world.get_world_by_stream_id(stream_id) @@ -56,7 +58,6 @@ class SubHeartflow: print("麦麦小脑袋转起来了") self.current_state.update_current_state_info() - personality_info = open("src/think_flow_demo/personality_info.txt", "r", encoding="utf-8").read() current_thinking_info = self.current_mind mood_info = self.current_state.mood related_memory_info = '' @@ -66,7 +67,7 @@ class SubHeartflow: prompt = "" prompt += f"你刚刚在做的事情是:{schedule_info}\n" # prompt += f"麦麦的总体想法是:{self.main_heartflow_info}\n\n" - prompt += f"{personality_info}\n" + prompt += f"{self.personality_info}\n" prompt += f"现在你正在上网,和qq群里的网友们聊天,群里正在聊的话题是:{message_stream_info}\n" prompt += f"你想起来{related_memory_info}。" prompt += f"刚刚你的想法是{current_thinking_info}。" @@ -84,7 +85,6 @@ class SubHeartflow: # print("麦麦脑袋转起来了") self.current_state.update_current_state_info() - personality_info = open("src/think_flow_demo/personality_info.txt", "r", encoding="utf-8").read() current_thinking_info = self.current_mind mood_info = self.current_state.mood related_memory_info = 'memory' @@ -93,7 +93,7 @@ class SubHeartflow: reply_info = reply_content prompt = "" - prompt += f"{personality_info}\n" + prompt += f"{self.personality_info}\n" prompt += f"现在你正在上网,和qq群里的网友们聊天,群里正在聊的话题是:{message_stream_info}\n" prompt += f"你想起来{related_memory_info}。" prompt += f"刚刚你的想法是{current_thinking_info}。" @@ -114,12 +114,11 @@ class SubHeartflow: async def judge_willing(self): # print("麦麦闹情绪了1") - personality_info = open("src/think_flow_demo/personality_info.txt", "r", encoding="utf-8").read() current_thinking_info = self.current_mind mood_info = self.current_state.mood # print("麦麦闹情绪了2") prompt = "" - prompt += f"{personality_info}\n" + prompt += f"{self.personality_info}\n" prompt += "现在你正在上网,和qq群里的网友们聊天" prompt += f"你现在的想法是{current_thinking_info}。" prompt += f"你现在{mood_info}。" diff --git a/src/think_flow_demo/personality_info.txt b/src/think_flow_demo/personality_info.txt index a95988190..d7b9e4ecf 100644 --- a/src/think_flow_demo/personality_info.txt +++ b/src/think_flow_demo/personality_info.txt @@ -1 +1,3 @@ +// 为了解决issue-589,已经将心流引用的内容改为了bot_config.toml中的prompt_personality +// 请移步配置文件进行更改 你叫麦麦,你19岁,是一个大二的女大学生,你有一头黑色短发,你会刷贴吧 \ No newline at end of file From 3b23be001ea6de73eb39357d08a1d1810f8ac4a1 Mon Sep 17 00:00:00 2001 From: AL76 <735756072@qq.com> Date: Thu, 27 Mar 2025 02:04:26 +0800 Subject: [PATCH 31/46] =?UTF-8?q?fix:=20=E5=90=8C=E6=AD=A5=E5=BF=83?= =?UTF-8?q?=E6=B5=81=E4=B8=8E=E6=9C=AC=E4=BD=93=E4=BD=BF=E7=94=A8=E7=9A=84?= =?UTF-8?q?prompt=EF=BC=8C=E4=BF=9D=E6=8C=81=E4=BA=BA=E8=AE=BE=E4=B8=80?= =?UTF-8?q?=E8=87=B4=EF=BC=88=E5=88=9A=E5=88=9A=E5=B0=91=E6=94=B9=E4=BA=86?= =?UTF-8?q?=E4=B8=AA=E6=96=87=E4=BB=B6=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/think_flow_demo/heartflow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/think_flow_demo/heartflow.py b/src/think_flow_demo/heartflow.py index a77e8485b..dcdbe508c 100644 --- a/src/think_flow_demo/heartflow.py +++ b/src/think_flow_demo/heartflow.py @@ -1,7 +1,7 @@ from .current_mind import SubHeartflow from src.plugins.moods.moods import MoodManager from src.plugins.models.utils_model import LLM_request -from src.plugins.chat.config import global_config +from src.plugins.chat.config import global_config, BotConfig from src.plugins.schedule.schedule_generator import bot_schedule import asyncio from src.common.logger import get_module_logger, LogConfig, HEARTFLOW_STYLE_CONFIG # noqa: E402 @@ -46,7 +46,7 @@ class Heartflow: logger.info("麦麦大脑袋转起来了") self.current_state.update_current_state_info() - personality_info = open("src/think_flow_demo/personality_info.txt", "r", encoding="utf-8").read() + personality_info = " ".join(BotConfig.PROMPT_PERSONALITY) current_thinking_info = self.current_mind mood_info = self.current_state.mood related_memory_info = 'memory' @@ -91,7 +91,7 @@ class Heartflow: return await self.minds_summary(sub_minds) async def minds_summary(self,minds_str): - personality_info = open("src/think_flow_demo/personality_info.txt", "r", encoding="utf-8").read() + personality_info = " ".join(BotConfig.PROMPT_PERSONALITY) mood_info = self.current_state.mood prompt = "" From bf8fea15a2c34889cea4545ad9d542677b41e0d2 Mon Sep 17 00:00:00 2001 From: meng_xi_pan <1903647908@qq.com> Date: Thu, 27 Mar 2025 07:20:31 +0800 Subject: [PATCH 32/46] =?UTF-8?q?=E5=85=B3=E7=B3=BB=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E6=94=B9=E8=BF=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/logger.py | 19 ++++++ src/plugins/chat/bot.py | 2 +- src/plugins/chat/llm_generator.py | 39 +++++++----- src/plugins/chat/relationship_manager.py | 81 +++++++++++++++--------- src/plugins/moods/moods.py | 16 +++-- 5 files changed, 104 insertions(+), 53 deletions(-) diff --git a/src/common/logger.py b/src/common/logger.py index 8a9d08926..c8e604df5 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -105,6 +105,24 @@ MOOD_STYLE_CONFIG = { }, } +# relationship +RELATIONSHIP_STYLE_CONFIG = { + "advanced": { + "console_format": ( + "{time:YYYY-MM-DD HH:mm:ss} | " + "{level: <8} | " + "{extra[module]: <12} | " + "关系 | " + "{message}" + ), + "file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 关系 | {message}"), + }, + "simple": { + "console_format": ("{time:MM-DD HH:mm} | 关系 | {message}"), + "file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 关系 | {message}"), + }, +} + SENDER_STYLE_CONFIG = { "advanced": { "console_format": ( @@ -217,6 +235,7 @@ SENDER_STYLE_CONFIG = SENDER_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else SENDER LLM_STYLE_CONFIG = LLM_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else LLM_STYLE_CONFIG["advanced"] CHAT_STYLE_CONFIG = CHAT_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else CHAT_STYLE_CONFIG["advanced"] MOOD_STYLE_CONFIG = MOOD_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else MOOD_STYLE_CONFIG["advanced"] +RELATIONSHIP_STYLE_CONFIG = RELATIONSHIP_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else RELATIONSHIP_STYLE_CONFIG["advanced"] SCHEDULE_STYLE_CONFIG = SCHEDULE_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else SCHEDULE_STYLE_CONFIG["advanced"] HEARTFLOW_STYLE_CONFIG = HEARTFLOW_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else HEARTFLOW_STYLE_CONFIG["advanced"] diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index e89375217..0837347ef 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -298,7 +298,7 @@ class ChatBot: ) # 使用情绪管理器更新情绪 - self.mood_manager.update_mood_from_emotion(emotion[0], global_config.mood_intensity_factor) + self.mood_manager.update_mood_from_emotion(emotion, global_config.mood_intensity_factor) async def handle_notice(self, event: NoticeEvent, bot: Bot) -> None: """处理收到的通知""" diff --git a/src/plugins/chat/llm_generator.py b/src/plugins/chat/llm_generator.py index 7b032104a..d5a700988 100644 --- a/src/plugins/chat/llm_generator.py +++ b/src/plugins/chat/llm_generator.py @@ -144,18 +144,25 @@ class ResponseGenerator: try: # 构建提示词,结合回复内容、被回复的内容以及立场分析 prompt = f""" - 请根据以下对话内容,完成以下任务: - 1. 判断回复者的立场是"supportive"(支持)、"opposed"(反对)还是"neutrality"(中立)。 - 2. 从"happy,angry,sad,surprised,disgusted,fearful,neutral"中选出最匹配的1个情感标签。 - 3. 按照"立场-情绪"的格式输出结果,例如:"supportive-happy"。 + 请严格根据以下对话内容,完成以下任务: + 1. 判断回复者对被回复者观点的直接立场: + - "支持":明确同意或强化被回复者观点 + - "反对":明确反驳或否定被回复者观点 + - "中立":不表达明确立场或无关回应 + 2. 从"开心,愤怒,悲伤,惊讶,平静,害羞,恐惧,厌恶,困惑"中选出最匹配的1个情感标签 + 3. 按照"立场-情绪"的格式直接输出结果,例如:"反对-愤怒" - 被回复的内容: - {processed_plain_text} + 对话示例: + 被回复:「A就是笨」 + 回复:「A明明很聪明」 → 反对-愤怒 - 回复内容: - {content} + 当前对话: + 被回复:「{processed_plain_text}」 + 回复:「{content}」 - 请分析回复者的立场和情感倾向,并输出结果: + 输出要求: + - 只需输出"立场-情绪"结果,不要解释 + - 严格基于文字直接表达的对立关系判断 """ # 调用模型生成结果 @@ -165,18 +172,20 @@ class ResponseGenerator: # 解析模型输出的结果 if "-" in result: stance, emotion = result.split("-", 1) - valid_stances = ["supportive", "opposed", "neutrality"] - valid_emotions = ["happy", "angry", "sad", "surprised", "disgusted", "fearful", "neutral"] + valid_stances = ["支持", "反对", "中立"] + valid_emotions = ["开心", "愤怒", "悲伤", "惊讶", "害羞", "平静", "恐惧", "厌恶", "困惑"] if stance in valid_stances and emotion in valid_emotions: return stance, emotion # 返回有效的立场-情绪组合 else: - return "neutrality", "neutral" # 默认返回中立-中性 + logger.debug(f"无效立场-情感组合:{result}") + return "中立", "平静" # 默认返回中立-平静 else: - return "neutrality", "neutral" # 格式错误时返回默认值 + logger.debug(f"立场-情感格式错误:{result}") + return "中立", "平静" # 格式错误时返回默认值 except Exception as e: - print(f"获取情感标签时出错: {e}") - return "neutrality", "neutral" # 出错时返回默认值 + logger.debug(f"获取情感标签时出错: {e}") + return "中立", "平静" # 出错时返回默认值 async def _process_response(self, content: str) -> Tuple[List[str], List[str]]: """处理响应内容,返回处理后的内容和情感标签""" diff --git a/src/plugins/chat/relationship_manager.py b/src/plugins/chat/relationship_manager.py index f4cda0662..5bc60cc80 100644 --- a/src/plugins/chat/relationship_manager.py +++ b/src/plugins/chat/relationship_manager.py @@ -1,6 +1,6 @@ import asyncio from typing import Optional -from src.common.logger import get_module_logger +from src.common.logger import get_module_logger, LogConfig, RELATIONSHIP_STYLE_CONFIG from ...common.database import db from .message_base import UserInfo @@ -8,7 +8,12 @@ from .chat_stream import ChatStream import math from bson.decimal128 import Decimal128 -logger = get_module_logger("rel_manager") +relationship_config = LogConfig( + # 使用关系专用样式 + console_format=RELATIONSHIP_STYLE_CONFIG["console_format"], + file_format=RELATIONSHIP_STYLE_CONFIG["file_format"], +) +logger = get_module_logger("rel_manager", config=relationship_config) class Impression: @@ -270,19 +275,21 @@ class RelationshipManager: 3.人维护关系的精力往往有限,所以当高关系值用户越多,对于中高关系值用户增长越慢 """ stancedict = { - "supportive": 0, - "neutrality": 1, - "opposed": 2, + "支持": 0, + "中立": 1, + "反对": 2, } valuedict = { - "happy": 1.5, - "angry": -3.0, - "sad": -1.5, - "surprised": 0.6, - "disgusted": -4.5, - "fearful": -2.1, - "neutral": 0.3, + "开心": 1.5, + "愤怒": -3.5, + "悲伤": -1.5, + "惊讶": 0.6, + "害羞": 2.0, + "平静": 0.3, + "恐惧": -2, + "厌恶": -2.5, + "困惑": 0.5, } if self.get_relationship(chat_stream): old_value = self.get_relationship(chat_stream).relationship_value @@ -301,9 +308,12 @@ class RelationshipManager: if old_value > 500: high_value_count = 0 for _, relationship in self.relationships.items(): - if relationship.relationship_value >= 850: + if relationship.relationship_value >= 700: high_value_count += 1 - value *= 3 / (high_value_count + 3) + if old_value >= 700: + value *= 3 / (high_value_count + 2) # 排除自己 + else: + value *= 3 / (high_value_count + 3) elif valuedict[label] < 0 and stancedict[stance] != 0: value = value * math.exp(old_value / 1000) else: @@ -316,27 +326,20 @@ class RelationshipManager: else: value = 0 - logger.info(f"[关系变更] 立场:{stance} 标签:{label} 关系值:{value}") + level_num = self.calculate_level_num(old_value+value) + relationship_level = ["厌恶", "冷漠", "一般", "友好", "喜欢", "暧昧"] + logger.info( + f"当前关系: {relationship_level[level_num]}, " + f"关系值: {old_value:.2f}, " + f"当前立场情感: {stance}-{label}, " + f"变更: {value:+.5f}" + ) await self.update_relationship_value(chat_stream=chat_stream, relationship_value=value) def build_relationship_info(self, person) -> str: relationship_value = relationship_manager.get_relationship(person).relationship_value - if -1000 <= relationship_value < -227: - level_num = 0 - elif -227 <= relationship_value < -73: - level_num = 1 - elif -73 <= relationship_value < 227: - level_num = 2 - elif 227 <= relationship_value < 587: - level_num = 3 - elif 587 <= relationship_value < 900: - level_num = 4 - elif 900 <= relationship_value <= 1000: - level_num = 5 - else: - level_num = 5 if relationship_value > 1000 else 0 - + level_num = self.calculate_level_num(relationship_value) relationship_level = ["厌恶", "冷漠", "一般", "友好", "喜欢", "暧昧"] relation_prompt2_list = [ "冷漠回应", @@ -356,6 +359,24 @@ class RelationshipManager: f"你对昵称为'({person.user_info.user_id}){person.user_info.user_nickname}'的用户的态度为{relationship_level[level_num]}," f"回复态度为{relation_prompt2_list[level_num]},关系等级为{level_num}。" ) + + def calculate_level_num(self, relationship_value) -> int: + """关系等级计算""" + if -1000 <= relationship_value < -227: + level_num = 0 + elif -227 <= relationship_value < -73: + level_num = 1 + elif -73 <= relationship_value < 227: + level_num = 2 + elif 227 <= relationship_value < 587: + level_num = 3 + elif 587 <= relationship_value < 900: + level_num = 4 + elif 900 <= relationship_value <= 1000: + level_num = 5 + else: + level_num = 5 if relationship_value > 1000 else 0 + return level_num relationship_manager = RelationshipManager() diff --git a/src/plugins/moods/moods.py b/src/plugins/moods/moods.py index 986075da0..4cc115d3b 100644 --- a/src/plugins/moods/moods.py +++ b/src/plugins/moods/moods.py @@ -55,13 +55,15 @@ class MoodManager: # 情绪词映射表 (valence, arousal) self.emotion_map = { - "happy": (0.8, 0.6), # 高愉悦度,中等唤醒度 - "angry": (-0.7, 0.7), # 负愉悦度,高唤醒度 - "sad": (-0.6, 0.3), # 负愉悦度,低唤醒度 - "surprised": (0.4, 0.8), # 中等愉悦度,高唤醒度 - "disgusted": (-0.8, 0.5), # 高负愉悦度,中等唤醒度 - "fearful": (-0.7, 0.6), # 负愉悦度,高唤醒度 - "neutral": (0.0, 0.5), # 中性愉悦度,中等唤醒度 + "开心": (0.8, 0.6), # 高愉悦度,中等唤醒度 + "愤怒": (-0.7, 0.7), # 负愉悦度,高唤醒度 + "悲伤": (-0.6, 0.3), # 负愉悦度,低唤醒度 + "惊讶": (0.2, 0.8), # 中等愉悦度,高唤醒度 + "害羞": (0.5, 0.2), # 中等愉悦度,低唤醒度 + "平静": (0.0, 0.5), # 中性愉悦度,中等唤醒度 + "恐惧": (-0.7, 0.6), # 负愉悦度,高唤醒度 + "厌恶": (-0.4, 0.4), # 负愉悦度,低唤醒度 + "困惑": (0.0, 0.6), # 中性愉悦度,高唤醒度 } # 情绪文本映射表 From 59e1993787418014e175f70021165f217e8f8ef9 Mon Sep 17 00:00:00 2001 From: meng_xi_pan <1903647908@qq.com> Date: Thu, 27 Mar 2025 07:51:10 +0800 Subject: [PATCH 33/46] ruff --- src/common/logger.py | 4 ++-- src/plugins/chat/relationship_manager.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/common/logger.py b/src/common/logger.py index c8e604df5..68de034ed 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -106,7 +106,7 @@ MOOD_STYLE_CONFIG = { } # relationship -RELATIONSHIP_STYLE_CONFIG = { +RELATION_STYLE_CONFIG = { "advanced": { "console_format": ( "{time:YYYY-MM-DD HH:mm:ss} | " @@ -235,7 +235,7 @@ SENDER_STYLE_CONFIG = SENDER_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else SENDER LLM_STYLE_CONFIG = LLM_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else LLM_STYLE_CONFIG["advanced"] CHAT_STYLE_CONFIG = CHAT_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else CHAT_STYLE_CONFIG["advanced"] MOOD_STYLE_CONFIG = MOOD_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else MOOD_STYLE_CONFIG["advanced"] -RELATIONSHIP_STYLE_CONFIG = RELATIONSHIP_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else RELATIONSHIP_STYLE_CONFIG["advanced"] +RELATION_STYLE_CONFIG = RELATION_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else RELATION_STYLE_CONFIG["advanced"] SCHEDULE_STYLE_CONFIG = SCHEDULE_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else SCHEDULE_STYLE_CONFIG["advanced"] HEARTFLOW_STYLE_CONFIG = HEARTFLOW_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else HEARTFLOW_STYLE_CONFIG["advanced"] diff --git a/src/plugins/chat/relationship_manager.py b/src/plugins/chat/relationship_manager.py index 5bc60cc80..54bf8ca11 100644 --- a/src/plugins/chat/relationship_manager.py +++ b/src/plugins/chat/relationship_manager.py @@ -1,6 +1,6 @@ import asyncio from typing import Optional -from src.common.logger import get_module_logger, LogConfig, RELATIONSHIP_STYLE_CONFIG +from src.common.logger import get_module_logger, LogConfig, RELATION_STYLE_CONFIG from ...common.database import db from .message_base import UserInfo @@ -10,8 +10,8 @@ from bson.decimal128 import Decimal128 relationship_config = LogConfig( # 使用关系专用样式 - console_format=RELATIONSHIP_STYLE_CONFIG["console_format"], - file_format=RELATIONSHIP_STYLE_CONFIG["file_format"], + console_format=RELATION_STYLE_CONFIG["console_format"], + file_format=RELATION_STYLE_CONFIG["file_format"], ) logger = get_module_logger("rel_manager", config=relationship_config) From 6e63863e19df8dba23db64789ef64c9873ea5973 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 27 Mar 2025 15:57:07 +0800 Subject: [PATCH 34/46] =?UTF-8?q?fix=20=E6=96=87=E6=A1=A3=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E5=92=8C=E8=A1=A5=E5=85=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docker_deploy.md | 2 +- src/plugins/schedule/schedule_generator.py | 4 ++-- src/think_flow_demo/L{QA$T9C4`IVQEAB3WZYFXL.jpg | Bin 0 -> 60448 bytes src/think_flow_demo/SKG`8J~]3I~E8WEB%Y85I`M.jpg | Bin 0 -> 93248 bytes src/think_flow_demo/ZX65~ALHC_7{Q9FKE$X}TQC.jpg | Bin 0 -> 90138 bytes template/bot_config_template.toml | 1 + 6 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 src/think_flow_demo/L{QA$T9C4`IVQEAB3WZYFXL.jpg create mode 100644 src/think_flow_demo/SKG`8J~]3I~E8WEB%Y85I`M.jpg create mode 100644 src/think_flow_demo/ZX65~ALHC_7{Q9FKE$X}TQC.jpg diff --git a/docs/docker_deploy.md b/docs/docker_deploy.md index 38eb54440..d135dd584 100644 --- a/docs/docker_deploy.md +++ b/docs/docker_deploy.md @@ -1,6 +1,6 @@ # 🐳 Docker 部署指南 -## 部署步骤 (推荐,但不一定是最新) +## 部署步骤 (不一定是最新) **"更新镜像与容器"部分在本文档 [Part 6](#6-更新镜像与容器)** diff --git a/src/plugins/schedule/schedule_generator.py b/src/plugins/schedule/schedule_generator.py index 41cf187e1..f4bbb42b0 100644 --- a/src/plugins/schedule/schedule_generator.py +++ b/src/plugins/schedule/schedule_generator.py @@ -139,8 +139,8 @@ class ScheduleGenerator: prompt += f"你之前做了的事情是:{previous_doings},从之前到现在已经过去了{self.schedule_doing_update_interval/60}分钟了\n" #noqa: E501 if mind_thinking: prompt += f"你脑子里在想:{mind_thinking}\n" - prompt += f"现在是{now_time},结合你的个人特点和行为习惯," - prompt += "推测你现在做什么,具体一些,详细一些\n" + prompt += f"现在是{now_time},结合你的个人特点和行为习惯,注意关注你今天的日程安排和想法,这很重要," + prompt += "推测你现在和之后做什么,具体一些,详细一些\n" prompt += "直接返回你在做的事情,不要输出其他内容:" return prompt diff --git a/src/think_flow_demo/L{QA$T9C4`IVQEAB3WZYFXL.jpg b/src/think_flow_demo/L{QA$T9C4`IVQEAB3WZYFXL.jpg new file mode 100644 index 0000000000000000000000000000000000000000..186b34de2c8115074978456317fd795e4e5f2ac3 GIT binary patch literal 60448 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 t2KxJ7o!@A%9I$jiu`OI8{sz9VgVg{4 literal 0 HcmV?d00001 diff --git a/src/think_flow_demo/SKG`8J~]3I~E8WEB%Y85I`M.jpg b/src/think_flow_demo/SKG`8J~]3I~E8WEB%Y85I`M.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dc86382f7685e111c164dbedb1d5704d41afb987 GIT binary patch literal 93248 zcmZs?W0YmvvNf8jw6oH-ZQHE0ZQHhO8kwKttC$mBc{*z58h+jn!t8VX}q`wB@Rx-^+#b$#bgpDa$40u0{Us?D=Nz5n=zb z`|Gpa{s7YZKwRZT66Y!xoPm*o8GxR|y?BV3FRIrcG`#{4!U|1K(HG$C8GzqjX&Y(l zgHsYaS_>L57(mb$zXx6yoDKvHNCq$W?P<$ra7C9{UMkAM7sG&UmM?)BFAs(=64JvP&`;vx`EpAwGu`U;`Wirq61M(joA1u~GvSyA`yQzGeD9yMQmv z!p4o<`)6CfWJJEVvb&?t`~SH|b7r7%%ijyTCgY4Cn!*5gQm$*xjX%ex{sF)o6sRq| z7j)168<1&Gzwdqc!Dopr1rrqX{qy}g7~tdLF;AT?wcbB){j+jUC4#PTf<{CLbM$|3 zga&-#=M(i}2*V_b@$Yx3 zBC)7uZeo%rD@)>)Qh4=gd$Ny1bwKa93D;l51!3+d=GL)C(C|}CmU5Q&dH+o8_ho#4vaveMFvnS$Zn`% z0-`8YsH2tCa2?Z+_2@XH= zO*H!4kkUW?Ds~dz_U)pXupnTHlT8I?Z15kR{w4;pv{7jMiD|*~U*cXR>=`Gypf~x6 z{`X%0=jM{Z>FUeW!=sqf@#U1jvDj?4_eTbx5GfkMQ#L^J%VyLLoTPyj z+h0;d6kO~N=J}|} zv*viQ%8V8qt|O#E`Cl1h&-r3C4~p@moyhAi2kC#5PsoZtT~Tqs!NG}%d5#zyci>u* zxFJFk$m3kKfKwBbgxBm5B=H*!M`M4z-^ENv#>XQD-jGlaOaowm@^3oMN!zo;?VPXF zPoy(Dbcsdf$$xMB9F4Y7IjS*^M}e+bcj117!FHQ(WUi7SdvG-YV32{4fEj_PvU;Vt z-el_W?#^Phf!rveigUYCY?4`oyLEQXGKEH~?O{NREH^t^34KOzZ5!E(l{2KTU9d1Qp~k|@0kr6nX>;_NAfTdE+Z8jdIP7+XB2cJ_ zIQDb@Z)=m~^j$C0x%|Md(j1Fwndi?^y_qU>EEozP>wxqHGh|z7gflPdu47p6^8$H{ z$>=~vqPeA|;5wyL9&XC<-|;Rb0WZbQsV14It}te(FRP0+diX2bf4G$UO7KlC;T8IN zaRd~Uk#81~5#~BG39++A{3X(F>hG*8|1&F^rVYx>R~#{b$bAT=WF*j1z}MQzu?&2) z4FZ`7SPCMlda(l>F%J;Nb;cLORZnrfupS(8F;5VT1Ql;PUNaS=9RyW@P6fFl9=>0V zthDMnw1SADRN1rD$HdOON!*UIAyw8b6*&4^F-0T7BqQ}kIPLGuX@3(vzkm4Q<@ zB2k!vrXphKRJtLQqxNB3=l_qN`erzaHwbwypkbHONAht*rmW7X;a$2voI=EiS$$_p z$isQd1Xnx*y>GqcvJl!{uD6b*&@LH+pj|=xsr&oh)k`;mz>ktJW|$|npw~nD-4fWI z&X?UCO{FxWC})48ZtDS(x>86+V9Y|^*GKRl>B^P=b)Ed@w3R||s0hRVwIjYt5#KcF z@ZIVKT$Nu!fx>?S1PgDv)>OIrgpQr4Fa9^nH46Uy4Z+;69I0IU2OoPX)Jb^O=Zwu%ojDB!rf@K6lAz-Qd9{$;z8-5O< zz8&<0E=kD#ZF>lsV>H2vQ3OF?TRfdvXqf%{zX?7S7Pm~;ZO6X=P&_hLDCP%b^R92X z@OmI*y4}O6lK38RGf;;4c{>EH0O#Ppac9$49MDJZGv^qAcTm!6;2&R5Ubw9l#>}Im zfSpIYq@n-whw+aA03l5yLK*bm%7&@MQqj@9Gy7=iR-xHi>)Y7)E^Idee2{m?!RJzB zYIVTy1Jn@=ZT|)_Qy%Yh68MTI$-5y!>F!$SwFyA zGW^X*e!}bbM;M#{6O7Y9ea92t!6)x{pNIb2`J!PBzMYTlf6_&e&douK81b(-MatHq z35RvMeY>O&48fuyI)R|$-&htU$8b1Y$0sKz7Z-oCVISRhHji);cR|yx;e%_JAYl8~ z6@Jm3oFkm-qyAUUZo~o{=^9dx4({P$%m5)%{l8KT8W8HQrWAi1`ttopa@z+7#G7Zi znPG!}1HXU^_|2Np*zaK1)QEjEkPjU&Ra#7rlc7K}_}^c^e_x1?l3s`n3%6m=C@goC zQj7XOb65ngDI;ON`4%+{I?BH#(zo9I)Wg3`VIJhl?)}-$ZuS3Mv8B9^U&xIsR=99I z2Z>-I9B)LrG!mEhGf73ecbI@oXCWPR)c?91kQO5rfHN{Y!x4qQaWaz~*v7;$v9uUl zXFCX#X__8JtY#7jWWBQ9v{*L@-;2yjRdrcK$4j_%Ix@w4T5UT~%(FKfach=tx#i!Q z_qs0X-=eo2BB?h33^Ph3@8Rgxt-6^5+Wq+^=w-~ zP|u$Eaz_#R1-m6vXat5!0RIhwB3+XhX;YzE>VK6KC&Ogu1VG<#Bj)PIMG zE`9l3fc*cVIW&-hSS5!w z?TG*->Om+VKq-f>il=e=e1EZ6s$w>s_8l2iYIJ1wp~lrn^>6)6ha7>16K#&%wpznr zS_eur-y+ztIxT@dBMv**^X=83O4~qitXrP-o;uv1a%jO~`6~n6=EAYV(xtiRw(F$V z8sAx#Fs25y!<0_|Qv?keZ`(6v055m$B)0q2+4GvRi)q^MJFbKxgKo^lkUj7YzG(5& zb3sQ1#-XjdL>$sOILTur` z87Bz!f3W*~8n1{Y^-5Dl956=QqB>jx-okXLR1sYkMTaaYYzIH*E9CYwk{vI$l(_w` zuVLZC!^8U;b|7$!4$S_QhIzE_ZdHN#q{1LmPMS1w0AYC$zMLd{>=gPu;t+bqw|0X5 zYG+^q3%(S;fA{~=x#Zt;p1!?enSjASm5nc_3?ebAUc-?{NkP#T*8N-1Kn)WeD9M*E z>T(-M`!5k=L6$8OKw{0k+ezKl>-UBxCfF_@aoKUBF`v#mA_W#04heIjuDE#$#Fzye zrO3!f)#xXA$!7K?&L&bWu$=!ypjhK?>7yf8?)0A@O=cw|O^65Ya%W@`N=}pQ+W*3Dwf^H-#4?;o|1`pm`>9xZt;+o|h>4^* zcr-rgB+eOS9JD$*ApFZ*)hj2j+UU2-J%f@)TF^Mg7}9Vzc7YdzRc4-d_dy6{f|*NM zMw+yF{IvKx5&*e4w8X4}WOxSO&JX~)CWZg|Ulsx=2<(+J)1m-objf_$VlR~mN?BGL z%Zwciu#Sw7dt+;-FP8b`-$!(-G2y!%4es|o;e44_qy!&Y>vR zC3jDYYu>sNc{pFL)y5GJ?(>z3MSZPir&@CLaNk{CN0#_4MlH0EeUwG-oV{y*9M-RJ zb1BWYOdcq>9d^$76LMg^x6m?#RgH?Amp|zr71`>bW;pX)5t}yh;o9O$Eq8nVzBa^v zS~9Z3Icg`e_eVgdCGGm8bHjd`@^#5(TK1!wCIvhyo7QKbNM5z@!4yM zc6j{!lrS&>qfce1=j9$r`V@`5DJ^hRuUACznF@T$ZzJdiq9xX;Ov`GM@v%4#++O=^ zY>519_5QHG96}yx9RyB^z9f=}qbHEqlPUSKd9`4l>(+L%`vC8Z%~c)ne7qk#8!U0Y znzUIYeBa~@8fWT3rMK}i(+_U_bC+3Frg|M7skoc!<33eEKX%1EL)!Wlr=H~v%8xeU&;>sg?)K@4yLPm$^GwAv3-Yf`JB-Xda zHghw5?D6UKAp>J;5lt&c2-)jKahU&%qw97enf-~i$r(jMo5zvyWD{%?jqCSYUQGXa z8*3?Eh^cFBlB0T*l`y=#T#ZCx1h&~IloFDMQQ5s7?uD&SOQw1uWj~3W@_-CXR*Rj% zz$tIn{Tzgbd*{m-+e zt#xLn*A!XLdsjg>h+R!Vth1HIEyC)kh)>!gFx3-5t#ulqUg9w~YAXyiR#5Q*k28{chY#$YD zV%{B78D~W=cROU2E7*Ea9Y2S;o;%5s)^)11hV5Qw0^Car#|p6VJ#|+?QrqWbUA@Ut zn)l{92ohP2zFH`+aF^XwZtI=VX&p322IopHNKrqIaL3bDSF;gIHOE~)%VK^7rEKw1 zl6KU{XL=uuBA<3B{#a^2Z6>7Isjdm2-@ELJ+`K&A&GBiAD2 zmD~djT0_L#S}3yFs>()%Y9_9(8H|q~6trYmW-dCwds8fGQ`&n!yjtUK1kPEftJ&)R zM#$st8=X2#;R~v34>87VfDDzPEaDgb#w}juo8(LqA-5p8g4xRecpf$ z5%cgiMG)lwIuocSN)Jzsx^7Q|cW6LVsL3k{jiBC^dLY@ENQCQl3O4-waiY_?`q~&% zuqdpRy8H#p7FOIi^YlBzx@}=@O@S6(4j;dellJvZd%eL79t`Y@4652f0EWp|IS>c>qTIp*}9X1g={y=M+B=>RttY-uwJMI&vVol<}r12CE0-YavdVvbd_u{ z6OYFu6_{a#(0%p<*8|5d+F}X$;i@Yr`w_G~gr-$>#aC8>PNpK0wWj$EVnpFkHHu28 zHVT6uw@``$$SE$aBN?prLpXo9cZ;SYo8_ORFk3s`OdK@Cv9Bq9wU*J;hQmPFKXoiC zk4F?ak@*W_w!GCxdW0Z-tPNvSwv4!qEJqGEf0iM{fSZ%oyj*oVy^K&<=vny7ms6Rs zV=f8SdzMIhSMV1!ED(xRG$zM=J={MA&umS9#eu0WbRWp9g&I$&nM)nN4Y=#O*p>La zyAH-Ix;}J+ex;Z^?2K|AVxm4^CuSb8b9jCFa?)h6^;(P>T%r7gPCZ9zrROTbD?=Sh z=1mUdsq@WXiw1<-FQQgUM=Xr&bbEU#I45GnItg>~o-FMUN-hr4prJE45{u*Lk&n zs)0MSjQp2L?fenn>mPg-Iw1}+`JE|RCID`&*>lK}{CTxM`#FJC2Aj;mlCMlaZq5t# zAW~PL&1u$(AbOg7=~UTV5`spx2Qk-|I5?Xdb+*OJL4l&`Gfm6eW#S@{F`Idk%{Jke z6~iubnHIh#1G|CGm9;}bwAHEC2M%aSHm+{LR3F4+^|IF;{lyWPFXzGg`yb&(xAAw0 zyPPi*uibtU5C>6Xs&OFYVQWY(K4WZeZO{-Q_69$7vM%msIK3Ect`3_(py%H2hBRH{ zea`K%x9~i-${?(%fbJd`U0D;zQ=|8N-5iA}7GndEen2d;th(+P z9CP~WO~Q3c-`1veFOEM`Di}<~HoKTGLqlZSjPi~l;s=srU7e(qJVl*r*E|zNxe0NqGEkvU* zh}V@f_}qpX-wSK9Z`$3ICxV_0BF|3hdw{RZ4G?)lBQav|GrA@0=Tr<)?O%KJ;x^8~ zs2E%Xw8L=l4BV8oh=tKEL17;fprgEPzL<`t-Hj0p_ACrc_6(u6W_%{%(qq4hQ4;8ey4s4o7WtsC_ z-GGG4#0|s$usV8%rZ}>uR`Z*E{EAZlv+M6am@Kp`g|c>kFWTufWc2#-W*@|kS)Yql zr^bgkVpp7nIY$$x)&mO%eMK@3SUN*twu9}3|!rkDzbfXV?D{fTYQR#Tksk2(%I>$`fh}?hDQ(=p-PKBxi-0{sOa-Ws@>6@ zp|_AH;7CLXd>qCKfkM8)09_|14P}5-5jbjPrS@|n*=FDa4t%$#FYydPqJ_-O5VDa> z2P-tZQ(Ci!$x$FnfP6pn1M7tb4vmH{_PKmd_WrZV$jqVlamJC1Ur*2&kY-;JAdO549=-iiWzXd@2h! zm%CMS1CnGwCQ74AciPyS8U@|tlk5=D(={^ZVw9==#&H0nq`S=(#}|cckvZ68+0@Vz zW|GwAYezrts06wPVS;$8(d+aHeWl9Om~0y2*KW`A<)TzaXKsyXaIwehF1}0OE9k^9 zvJn>&)?0bn+2u-6k{AZvIm;C+4$AJj!lcB;VE53V9XkBn-Pl}8kZL4j(UcH}#+h1y zxXCqg^K&6NY7j6CsP$9nj^I+6a$*+SE(8xfftPF1IR7eH&fEEj@2~Ir1$y)m4Oq#h zW6MVNt4nFQqLe$sB8~reJaRsxN}sgFm*mq z>gQ@z{P^=p>BQTLiGWbLp`;6oBa4-0&-A{)q{sP@PRIUHnNC9E@;)!KTdC~jcozB% zoca&X2dSa?;mRu{7Mu@|%DoQSY|%%K0B~ z!igjPKTXZBzK>cM+mN)X=4uvFp8EO(RE0^DDxc4LG2flpq4Bx1>h8@Ny~bHn0%(WR zQN^l#M;d3H(h(6A%hhZx)zqI4$>-dyPL8i({y?|1lvR|Yn7iGS&le?IkTOJp;Ee~w zB3pM4Xe`0RiJ@(GV}^z{EpIgzXz{Th9#%k2lEfDlLx?TaO(R^EBpj zVgAyvRwNGE&XeNp{L@v`S$l^@`dVXNjkAP$&3Jc25z=LTkR z`QsxSeafhI8z!IE3sSrpT;cK|tPq%+oQ83?ioDdCn_FXLb`JPDg71nM$D`Uup99uB zpBp4=tr>K~3` zdZQ%u&50W77mVX8INXA}vKkqHUftQsnr*HMKO}bR3Vb`IpBLYh#!-Yk}|Q9x$Fo z&u776w!&jkc&Q#Y8mJE02`c4VSe5VR;-c$c!qXKodW1PrBOgWE+hgQXYxTIwT(-jd zZeFIXq%m?vv^}lnNehHLUM7e+gO_T-5N7+p3iD{S0eO{jXvxV8k}sbQ2og6gtol&< z=n0DH+ghBg^AgsA*&+@YCob608dX^p+u z+n)^Hr+5??z{MC!wu#vLbbI}?FF&4)`aySNRGVhWevh+&W-4@QG6EN;-Q+p~qB9w{ zIUesJyoz_LnWtm8I3l0AHyqx}?$yM~!GeXZBu+UpGy;Uru#*31Wg*`vHNR z2MGI$daGInz9P=2No0XoYc`^ca2{oKdEu0T`Z-hM(p@A5mj@tX1V8(4e=RjK#Zmi?fCm*{#C}bOb$^hj zT|b42=k-^M@cZVk6I3GNKh$~ldZLb;wti!bhI%N8LfgWM+onL(&{b*D1=Q-)yFrV< z>JsO3pU97+lv!fIX^hj4a;xYwh(XF)@Wv<5c3Zz=hJ2_9Sg;;h!k%kUM}sQR`T6T} zdias|86vlYl+;V5^7Iy&JSY}b>60ut^Tk8z&x{CKB<4g37g!BF73ZHz zOZwaKs`c=vz<51{E%BXjGOxfi60S-*%+=Vr>i|4seA8FJBLws*LJuMIroIqvz)41F zR4j};nQ4mEYB-?B@2w8Fn0`@3USU8Ov zG@2lXH?o!NXniRVqN64V`tRh?Lh*#fY8_b_EW7pjG6kE~1C-Tl^8v{nQOR1G{teax zH=j~1Y>*Kn1dh&RUQ84!%~+aSeGfZsjo51A*ks-A4yMC~sBd zMes8oloKCb^1yRN7|r`nM(xZ&-5tp_@5}2ui*Y0Ez_eMyY4r?hB|M_&_-2Vr3Z&7T z)@7?fyBJdBLS7e-ERJxY0Z8NZ+Kou!7#yvuo+_2!SDsn=1Sl=0iA#a4Gjq7otGKAm zMgq46PGnb=Iq&bL?P_{(6JwifRTD1g7T%vA8psk;hQuPl(relSPFXKb>W*i-{=8p) zj5Xa;*le+1FpL>b0fpyqB_Abdo6C{Gk5 z{`X=1CFC8`J~?dnZ$ft7{r;kGjzUV6CB-xnCv?=z1nz%El=NVxQ{b)vf*;+dXPxU$ zrcaDFUFMPR^cT2741WA+9p%Zx$NRQOG^w7Gx`Dp)V4m-yw_cIzh00n^T*l}>eT=Xiqb zno90O=b6>8+BfkC+g^d*>Z1B<`i7iGZ2AF*-i%Q2Q&vS@Ub7R?9|a@`YqG^J!e+EW zGoOD_oB>dOWHO0V$i8z&nXVgb!tv&!eBMZzEwtXMOg1GB2;a$%%r;#xSV!g@e;h#p z3j}b!%^K9!1+Op=66NbhG$W6UtP62yx>h$iM*k%s&=IJn*(mz}CNkz7D01ljdI?HAkE zs$Mu+)?m4E8%CP#de!*^-(zfR_co}983JiSLlvepAi0THm*U^-H&F}T7_^` zu`?dUzwv1!h%wb{wjsTNa`p0)#hSkmhC zW-ucD^v6KE+eY(gAsgF6Ho-k2%4{F)j!@ieW@4hFK!JE>D?^te)w#B?cFD?}Jrx;n zX#w%pa+j+UBLWSdzRyVX;C5>32*2p66VQ-x(TU-AVzOoutMs^}?Rm#rg1)!<+bL%~ zPR4Bd)?kqSYZaqQDo6LU*ZgQ7wuTSSM-O+<6^w`ODyvv8LX%reQ|55Uytlo!x4T`A zmcsjIeVAnC0=f3Uj|z?xPI7}I?jKKJZ^E+b*<>pJR~%( zo=cQ_&a_OQaRH}Pl_NO)UUXVQKGt2i6M*!D5pK#X&!dW_7 z$LNipr+1uCh*zk7V$4_W5HZM8V<)snQ!nR*e10H&McyB;&c9^V8!(vO4u&y~N~B?Y z+>LO$G3>u0vvMX|y6v)jf<8!7WDGg7rVpOa%{Jt~V$3`( z3g|OybuWH|mE4P8(Q4P3yj$|74^oJ(_qNN=t2371(i0edyh1O7o)+9NYsY;l$Qj1T zxdNro9)8{IrJP(R zx^*P5x@ZjJXlZybFKTF*zfl(HOZy$me%%V?c%i+D2F3NzZ`u-%pWPa?HPt1G3Z z-HZ)h4^NE9a$Voj$Ttv~Z0@;mTyA=3yV;NIttfEFXP=)ez_oM}J`MJcUwN~HfUS{+ z71D~s6OxF_V^YRSgS2YlHxj^>>hgeQ3DgP=!AZ4`(#u2PK9T=P3C*BuFJAZg%**4NIIjAS(;&MeNnVV1VtTA77ivfg2kT zBtzuhy?l=V4SOWO^mmZNE1EtF_s_Rqb-EgMq_WwTn^>3*R4H2x4Z@gfGGl0*=7WS5 zRh3^5V`OH z0b3PE(Lpb=_*W3B4jK&y38`S*#G$`#uaAIR!{O(6xePMQR96fAWZ`|_OCYaq+qJ5j zWhU$&Tul^$**Tx2%=QNGoY`s>Ld-<6K`;c$3}w-8KZPghXxa5Ikm?^JR!9}cE%s2c z`jtP#5u(nzgoy&6;bgN9-#*uW++>*eFocu&N5$nw+U+gs4BVQIwj<A-#MJJDgF z8jWW)u57*zymWD-Jnk1<&%oFzz}`UyV=}k|eabgq0@HTzHt1e9%xkHotqcjoC0(AN zGB}IYz9Xm-SQ4*FEv9-e=Vw-=B1%Q$88nQi>-9x3+>A zt0zXR@5Q^IV8rN*j=d!Gq%%6UYOD_n3FNiu_aamh_DsW$NXC)O9~2FU-qku$X(gMp zbwHfZ3(g(Rl-?w(;jRYze7=B5f-#M&$p9jGm`<0{H1|3rKgdN82O!sit%6f8#HgBC zqBpI%h>7hX-d)7eBRD)6o_fA}fWE1lq`c!mn>!9NlpL6WKOW`awAyfAALn;6zIM%j zh{<1p`unRLYql?rOe1%xPNdL422zQ$oIqLCfewVK?fubdBw$U9c)y8S#gglN$I7G- zSJ&K8+~3Roe$wjdR!TOmG3pe-(+1GS1+kAn9^;}JnA}(WY_C};P^ue*xrs5A$sRCF zA@}xucID>g_IWu#?Eo~8WYaugG4tQkpCzr`3-ToJwXQKG^4n>F&A28Ph zs=6xk8Cv>0+=;+LOHg^7r8Y9x6>Vuu8GlD}Skc!HR#;@kJYW+#;_ANMq5IWGodzMu zpzzS^=ZD{0`S76ba^ZM(B^d&Zzom8-7yIx_6-iMip8Ftgc#HBjd~#f8b!-xvj8=20 zI$Z_@C7fB1+VjS7N4Sw`09EtAZsZdKtiQEKd%p%By}&Kh)%JWee0z7#1|}loFoR?1 z{`36FgqLSfv;tY$KZM7aT*F6KqW6ab0#8u!-MzJlEm^0s=j;5*?2tB(bSU;iw7^Yj%%C2p???Q^0s}MaBFfD}5ExLJWNu;iuzE zH;f3$83Ze;l?-TUPfntTmGP&c&#n59BaNr`Ri!}%vu$23itsHFV!2w6-w?F}vbo2Z z-DKG^sm(B^+jFGspMqbBxzZ;#A$vWASoD-`Pzw5sr_H!+>#Fak!7ZqdPcavGr&P)f ze6c_4MS4oW$+Kw>q1Z<+;0-E3z-*ciM z(mCm^4m-H}_u3bU6{wTnk8bV5Yb1>oM3~#2sS~i4hTpEkBIrr}4!mN=~g9OAhroxA$bX2I=gY*^~A0##_|AaL~xbW-3%h z7?YIq!ctsmCgBSXPM1KwZ>m0mVZtycjsJTUiXGpN2C8)OhEzC3s-)ip{OGipnjLf( z{AGGB$6_xpFVBNdqIx*JbbV*wnvcDq)=O2`Lq^N8Vtq6!mQHQ1Hw5xGt5IINR;vwV z-$S>Y`L(<}uzN7tAUcrz!%oFPuX=WjI?@>_IqX)yocI0tB;)euTV6;q)nhs`gJ-mG z;TtdO?_}ZJ3R(t8DX3XT)osX|l6gkgq&ASYr-OJ;qOxT3uJl?jv&&| zNq?}!7dXo{YqhAOa~3TQl@(QI+MP4AUqrY`3|WLaRD^cmQl8 z()D0@$+TwB(k3j4^WF=WU{&JU4yvgVwVUq0CGW!Or(ctE* zZ$jU7u5)FC@Jq%2HB_9@!6qDTK8?X>5K?7ea_p!D|Hdw>)*WBM8V8w`gT?Zpqtqqe z+oQITI#0{+9=?F~3TqZ+5S{AjI_cBKt3&8)`gw{I-@o!~P|*#IRYGy3g! z!x8PNq_g?;UV*le0+U_?)j9-HO1zWq8Cr&RL(0*^qhYE?@!Vu8%b4#8RNshjC%fp{ zHc)ym+p2t3Imrv__p0+&EQeH)P4@?iacoRXOm+2DK@5-kF!nNn6pia?|2-j0gb)3X zAdONUP#4u-X=!Px5#Mur3P+P@kLyPaCR9Wr9g+Ff z5LOVJE7ig#w%IcX_^)sk>y6m01-gi&>dlSpy=S$5%o%$xG{D&{uh?(vBT=b8?JJO5 zHn#fH%;<>j1^e6D&6yEYoMr?`jBO@!Sb3RgsO9P4qVC#*EaAd*u%1$_uFRBfqn32) zDn&h!vq+kv4=8NqMJrycd|+s)v+Az>h>zF=b98Mi?*Gn8F`M7-av~jubeLrF&^vi@ zuwPX77EK#b22(iSF*(~ywzv*PAYLKkRxgJqjKod#B{C3E5s9h|DkII)-#5DUpDt&X z2_n6HqHL&>U14|Xv6C7kJX@jGT|4LwXEdQ2l0l9hla2H)2|Yu=eaiKMVaXV= z^duc;^<{&7gw(k?YDS6i53&g9yTDZLMDcvH)3M4>vHp9H~AvNcw~5xIWG0SZmMNUr=t7xcvC^kH1h%cJ=vDOehkG#$uWJd`E8n&KW%{gQorYa`X)Q+8?rM^3>Il2B1h^f zoIwb)O!&h@MuS1;yi|3zk^qXeD1HKF=R-i#ESbF4r1_7qnMm&ZS6!->tOT;MpOA%j@VVO!;KO@l{<2`qv4sPFZt*BhME&5hJCXL;{jpOyX1 zt&zA(XR{R`-{WVwRrtnpd4eHy{f;|*L8YZ+XdJ4V3I;8fH{VU{G&(&X9@kd)2lI-L zq{?_mvIj3qrj><#^bdDM^jF-K3PFsUgANyI&Hm8khD6s9MI^+D;)b$bo@TJAw>VdA zHSa(7FGOR8jW_+9T{|@`T!gbyb#=+0`kr(+DOQH76NFVTTV4wsLnM6-h{9$g5ooT6KYo1295Z)q zCQ%tnPM{8vyI>#lh;Z5L{Xt?G0k@vRb-7~q%(@elvV_CmbuRRoEL63<+e{2!ZvNyC z_MRigZKfk>w1HgIB`lK=7!&jJekocJ0&&darD-o}&`ac&W9ONIUT2)P!xvjH!sG3J z99)NcZrZ?jG0p$p>>=u8nFHOlep78?Ft%9}t%aVWQC1z;IX?-@|2A$`dAdd<>FOLB#jaT%WxF z>U?;cDA#5Y0|Nsc1e57Bo=rP90P+?Onrw^n<@(D`2=3#>+S2XHb$CgEw!zZz{!Ln3 zh=FFddA<@$OgcO1(X^M{6!#`10>2;;rt^!`i`k7$Zw8U}^Q^o!4>InGa8FgPWTxvaIm4NX07l4MH?)d7~0}*MK2`#gET@ zLG-%v>vk!9A{eOtgdLEa^gC=&R~ojSw2@-1DP|nI>lbPCvGZ=(wuceW0D1PSw05Xn zB@D^v`8yIN$?#!Bs$v2%cH6HHocpCH6<+yY<^|ZsPQx_VHiK!8{bqphX@farw%0%bLluXD?i)S~;dJLT7cAN^z>8*<~ zPmN9#TIz{YToHqTf_|PfFH>9Xs4y7suzGnOeZ^)hghe+!UtSEaW1r;?`oR9Qo+$`h zf(Mn=bd-#68y=>nE@w?MI`v{tu#gRnLNW^&cM}7}1@;B3RvPA8o$Qy*mil@gA;Aqz z#+9%>-!B4?5Mwv-M$FOZbWw%nl}W6DibKW@R_}UyG!p#L`K1U630Y%@sgZJxudNtC zd%LUBLRTlLZyKM@!iL5@D9i`@xDpJ5x!PnEVJm#&-^A{x+V5fF^&$%4GC@IP7FVwV z9Fuda-FW_1>RC&SuxTa)1OAV)NWIwdukW|55>`+4vA>|J-BfbbOTy-; z%=*bhofUFD9A>^15sJZL}&M@7Hc` zLi|OM094*BtXJ6#{1Ym$m@MIU=_Nc-h`z;h`xz*5e>orzr#%K26U%ki{G)q$fsl2YQE-QEb>KC35 zdvHS=b7I9qD!h`!8*ex58mbFyXBQx|odM8`m(oA9^iQ}vRdz*@z;{mR9k00AXD?xn zkj~^~95g$ zFSSyWygW_O zh1|!c7b4k$4E8g^b4cehDD^zUQJ)50`6uz$0m%yMiP*?)0cJi`wc36}tEUm_EHzZ% zcLvUhhYX5+W@)3}N&-fu3@s+uuJZ(fh6V3|-6A=O;YeB+c8wWW+go$a2|=V(y~KQ% zJS~+DL=8LCDbUe|%EL_oN?H$kzn6cQu8cHM{t{_gb zq5=N%kX|u`-K#yeH0Y!iH8AEn9G=iBm!oX-bDj2o~%8kwcAEMPPc>NoFvuz%W#&^SBYu)>yFZnPoG9$`Gzf`ZF(I=0f%c_E(#$aL>Z%`7P>&cr-S1^t1Ai zTUo6I;}-6YVB90Lmi74UB@)-w5;CDWy6paWB!U-W=DznecEX9cf?bMr@aullJF4es zR`<%>+OEk^2Pv}~F9q{+Bemc{rQq9(%-cdD_e%5HqnMr#VjIn=|8WFdNy#OZ5Su4} zo3~i?6~p#+_#Mxh z`MUk*U%LxvAN}=R&xLcG*L-mfY>aLkp9pW{?x*9WGx#vf2DTQ3!+c5JoxDv^q7@DJ zHP|XE4b8`Ig~vBY3gV)V&@Xrv!>*r|04}PuoEm}DUn=Bp@CMrUL@#oYJcVGlu|Z=D zI{WLF_nYDwdkL_j%Q?i*X|03KJc2U)_Hs*%`X9SpD3XhMgyTW*qXS{()MGABQY-Hd zU!dg`J$kj9ocaOe*I9Y^Z0EF9e>nel7C=6gp0;6v%WEmF-bBjBT{pKmn{s3v?{f+o zpS!cSi`_;13Wrhb$(xW}-Kne#cCte(W0p=ndtSH4F~pr*IPh;krH(3fzEpGtjs8d3#?z975;bGUHeEX{Raik)_p zR+<&Xq&p{*+`Z|8`*)YHE_%&^9-sEGXt~AUL=yj=i`=MXt~?D$XHh(KG|lRjVyL~f z`8j7vOM6!XKhib+einANX4iiMcQ6oq>aAVG^LZHgSAe7>ZJBHFaklMzwoALbOn0#c zlMf%Hl9192RK4_|C~H&^i-oTLFD~?4jx|R;!4_$Vk(VTWC5rg}N7q+|Rn>Og(hbtx zDV@?tcS?6`y1S&iq`RcMOS-!S>27H$k>)IX-uL-VT<4cRkj38Xo^y^dYut4^a_S<2 zOAwDbLix0^Rt63>= z!j*<1g{TfGIM(1l`J$Sj?oa&34kj~86myuSzBq)afWxgMzNev>xBqtZ_^Uf?2GU** zJKKH#oehOeSfJuXv3%x+@8cQzJ7|ApeLS6*W~L2y~yGVLslc{DK)Yg@;&)^mE9&G4&5o$ccC2KZbPIGLsPO0~y{ zzXK+3?!Lft@bCP-YxyadFh?5clc=v04XWa|=xnDnA z(c>c=wds8koW>PANn-?6=6BI-UXQ5$_w;hyotanT#o-4E$cA706S$(MkX8=~nyh}z z8r>bDa7w~+^B{2`MvlG@pOVdFH98-pHAt9zOtWbpz!VIGCo)jKxb#!2HHbRpd0bc9sO9U`>dxM zgen2)01qttg3u<+jK&bm2vZn+pDu@gpDZGZE5#NZVa!;yv5Uu*DDNv@j2Fujxb5lJ zf1+hMz}DHUoAW>mCa)EOw7R#nDq^*g^#er$<0ulP1~OqsDt#c?T5feXw7ADS6YmPdy`Dx)g}BhR}q%&MD!k@0L*XZWI8Dx4f?@-0G% zA~hT#y7mdgl{V@^+;|GA;+j&vXUa&^UB|e65k1tDU44=9tSgF(Yf!B$n)Cd$Agw(G zj>T})@o-$RaLnjOpA|{?w&<9CrPR7DTU1}CMpdVY@9%mw!9Lw0MpS_Nu@Ryj_64F; zkY;4G2!D>vxEX_~WnciGT}7FrMRm`KBYn8k$(KI}CTTzp?=>yvX&;~G5nTQgv#Q{u){w6w-PhZbkG#Ca@N zE;^QHZBejne05ovZOGoK-BtR=J4&dnl~JZT^kZCBK0#(Vd1u1caVU_xu}e7HY2ulF zzEYl$I0@L3MYAG$Xr&Y;7Cl4yfcmzfo_lD8wiLSLE#PvfbotZOlIl)PhbX8BIumBI z(0T!hPlJA6#Op%Z5ZBxRGJJ3gNM~;kXZndyAsJdI`T6W{+1gcuOO#qEEfRIDuuPZd z$-m&xpkX}Vi%v)nK!v!_b}L1~1RD(NEu95B$r{(_;2Z07LuuMd=mqB+jCdgS;CZ(20^JgZf#A?lbhyV4dNKAxo`U1HD*11 zz-fYy#)POfc5-$$V(e`~CqL4pD3q}a@qpUX7Eu9qr3Dje=fQG%g5W>+y?BF#6z8kX zlZV1skOCXb7X5GqJLT{3K-J_2RxfN{66M}k2TlNMuW1=Dyze3S_fxe6M#^rJ=KP^b zQ=<`zxkop+w);Sdl%dplZZ=UVipw)q;wiC=a7`EBP2^H8$q3(6dbHbKeM4*;!b=cM zKs#whZ0T}UI?Y2VwvPPB{bTX$fs|G(@XSAU0)fR`*!z$bmb2h@U^P0;D9*+LUbmGZ z0~~R{HZT}-!H$)Anax0L(Ytd_hrDi8x8WKaeG8IB|40~0O>=OF^93yqmD;hmk6*11 zx6r+WAmtbQ#dx-7N2{Pbk(y}0^8e4qwh|OE>{stAX-6|hO%Wn}`RN=%5?ovzlsMYgoG1Qb>h8lyS=x2On(>%WbHKZ?*=>yH}@i*w`x(hRj@r7h)*$&t9X zaiCaIeJQ%+$lT)(p)%1%!LHJg{<--aO8W(lA>+`5qa~DdS&{jA+XvZ2F=DrN9*C(4 zcj&qpKM9YF65vrF;tVbYGzrd$J@?R-#+Q_utmf&O@T6?D`%2%$SN?9w=OV#zYZ}vv z+I}btZ_kux;oJJK)eB4F0&`O!8A~pBi_4SPdUe%9p<1C{W!T*NSyoy)-hQZ@f40)> zKO!uKa7k~Q`ejCk=VllkPR7f!OZYzudw-5f z(JUyL9jZ|bOb6x?luIQp5e00ke<%L?kaOT6#Tn#jIiMmvNCK<=^4n|Hj@_nsY!4jufCE~q9tvNC@DO+u!HOQ{uVL-&y- z5SBiDDG9+$98-rf`vEM?i~cb)qOc7^A-a$k@cs-POb5wI|6|X7e^mVKjr`U4@@Lme zh$QBj!LT&11h=Y;zR@yw)bB0LU>ztYWQBux=OazBD8vN34)Yp!t8WpAiBJX{w)>zf zTRCl(z6%pGJzK`I;^iaZ#e+vs&ve2FJCNy@K#F_E-8FCe{_y9Z=>4mm^tQ!cPu7Ed zH0R)~(g1(WDxb-zX3epKh=^F{96ntejW1d({=yO+LhnRB98$$VlmfEn;VG?GPN>ay zJYVB+oKJJ;vvap7<6rFuY=wK?{ArI5sC5vLvLZc>XeMsDP>;m~Ty_j) zDlH-Q5G^gie&b(QiH$0xUCJe$1}vCBvX*0DPKX$EF;Dv~;Iu`M+++mg$^xKEYfTRh z_Z8&Z%)}k5#f1SeQ8_ujCKDb0mL5mJRRNMdc)nBQFhMy-Zyq!Z4e|aCO*%~`bUHwJ zLtJ9e(L5fms2+{aX%9{}e%lAoZ=h;@(Ry*_JFVcOCORT(v_~S+Utuus508)LW@dE02olJ91mx7k^uhGHOd^vaLXhaW zgQal(QIJ+E%4OktUEX-w^06S6*6=~H;Q772Ep$?J*tsnc;?OkQ_hgmGAP=W90r$V) zHCAcJ+o{hC2MOL{96#_~^}@eX>}oe$2-_`apebmVd`d|;8yTOYN0>(&D_Bx?Mh92z zLS6{pnR~{M0%wMY?CRVSQhi2jye3RC+m~dY=xF1f@m2Sv5w*kZbDVO5bFvP_v%w_m zIahw6QE&X(ku5h9#Vo~_<{t7{Srw6HiO(JQ9xeuz%p*;k*|i}j-9sZ)cxSPc8>5+o z*s9Pvkwd#0hESJV(2oPGI-wr$c2=YXNtq+ijA+#|=$%S}$Uumr{#-t7p=RpxltJfH zJak?>WaE(s#~p{b5#vB73wmdfI|>~5FQ79C$&rvJLmYI8=Eq$dAg^sS+;wNx0lT?hy8LMX^(? zT@p6MYzBAu6Eg*pgPSuL4K9Uv08lvttqaRj4gbq3DC;&$p3Ev91kVId z`WGJEV=JRhW{6K!4>i(vg?3{pvsY;{>Q;NqDWuyKDmI)EDu67^);GAGWr39g!}K0o zohkewAZl>Of)QucT4qffcsaGvzP~tPT7E|v4mGY5xE#EkJEdsb@15{Uu5wr&EX1^Z zm4}c<-^@*w8m`1YD1k64SLf|0NF=5&qu3!m`Kba^v!o)C^FiXK?*7s=a!Nmd;L# zM$eUZp~Z&llP#HuaIwoc>F9B3IWK;r;3oJHr)n#>(y>#$Z7b`S(i;z~R4{imbm>ZY z6`WZ_QYG*L{1E8fdV8DaKHZ~1BF3EVQNMFj5?Q}>I%!!O+g;!==(alT!=!7n{i|71 z4)Ml7^3IE+Tx#i9Dhih@Gzqw0Cuq`VZ^b90w_6IXYo)r^<%7+PllPj!@D&-3ORLG zX+fJ29`I6~Xx%E*yIWd^B96cvK%}izcppUWopipDS2gnoY&%d87-o3hF zZ>ISa?U>{Pp)^|*xoGp}X&0Y=J0Iva?p8B-cgmdkF=b&F@5Yl|j}TX4!lsSYn<4Cx46NpllAal7YmSL#6fOkTUz+sY1wfdo8(&~-)3bnH zE{>?rd>Tb6GqEie(c{!8r)EPs`?@`OWgTrmTYFmkBgx(}$NSH2sOVF#ag8UphYs~P z3r&cd1Tf5zrWXRL4BT|ob#F3H|A<7Xl0jhIHdcuc(}gvj4@6+>WL%P_0%2~3FqWH- zCppw>@c9-X_nUZh!?7edPu7W^q%WkrQ@{Lo)Ux6m^raGV;tC@y{JBe-pe}jOwlDuo zk9t+RcQFq9x@BH`rlySV@ms9kn6+IK)!A4}dX#zn#&M)Aee~NpG8s)!E)u@c2n=IW z01H>x7J_-{I;P(RWPZ3W_JuR~Mg6NT)Eg2pN(`n?k;92Y4%D6_(Y&Ze(--Vok0KTo zy-_$3$Qf%GoLy)*RnK8bu3?QS%3J@xnvqU)Y1QIU*p2Mp4GaO*2>wv1FUd>aNx75` zc}yZ!C5FZ0j*+nLf`jpdyPIObWQ=T4CQA*vyF8jvN!1D4<4G32aMOSe#Ulw{UM33q z+lZxjPkuP>Na@XTO^QXiaK$TUhfbcWCOw~L8a$ga4t1uF!$L|-Vk`3T+R}Zv?h=oJ4oub0*BK9&E{j2j8a%|%ucZilC*vm! z;S)>LlkvlJ2xXb1Er*UB%xie2 zhRY{X!~jV4nLw3}ds%U+vC*mv0Cu5d3dfz6P+HH~3TwpWDg@@R(r6hJ8!DsJXKe?a zj-Mti!#6-Ai1q-olqrbb>m!r+gBkrmFji%G%UvMnd~lAwgO&(kQ3wy$ zS{uH9_INjpa~HhI1{-;W&?fMO!b$w2?r{Q-!OZ6j0p2qB$^x>PGuFC2BGl+5!Pyj> z&0*V3^HJfeB9q5Y<_e8*1qO+ubKQP^O@t0YMQ$SsRd7+}aH>2I7f2AWP#YT?Y8&Va zV1V=GUcHHu;(xQV<%EWU0;=d-pa$t^WQ|>cp#BI0aFnU^!*ePpXjWPzqm! z5$aO5_uqk3=iIOy-W9Z(=uyE8Vz)lwX0RzE(#N=J5jFgCzMhvC)Ai&9(9(`3L~d@FW$A%| zFBoMPF$4qgfS?s5deY^XS@5SkxNtRSqYt)uJBEPvEaRJ4=U`i*tX{(HTI4+IY1pSV zIo&!{%rgXJsU*o>7w%a_w32Qi89=#n);OTAihIin?Pr$=R6)=tAXmBUo zk3U*^9ZFTuRN?8PX$6p&woKADZzW_?p%Zh|j;z{uJ39=|js060| zIy_>Pp-fk*TnGpdLJfFW!freM8HDs$tjx^J9c@Bn^vRKFllTq; z7?_ublbXA)kc3j-oy{-a!3T+^`2~KUu*b4x|AIb9*+f2PC_)R4OqQz-g@;-81Ps^% zd8{aQGn0OKMJ^Wipx{Az^|hfFy>-9IMEKSfsD13{U-0VMsM z8em6}X%*tQ%t)+xH8VnkR%~xVq{-!mr5zwwq=%Zj1B3sbA}rG%S0Pf1v|hs=RU>_U z;%Lk7xdFg_g;JpvB%k}a_g=gtVKaExsdls78TUI`Q@9o~2wXYGR-=B+#J3CyL9C}2 zUlv-qEJjTYvH`yK_IUmTn6xY&*J%mav}JH=rCD2Tw(ovCZ?`|x%B`)f^}IefABC{S z672zpc)UI9Hzy%9G5o7Wi7W)DQCf*q5}RSTv8_8#3*h15B_ZQdEu9q(DPs-2UJjEu z%tWY(58JZfN87Gye?&j!rU5?De+n@Ew{V8aT8_@Ow~UTlk@NjF5LQ5qgv9O0*o(Ul z$lXGIP%QP?g-~bQ?57%phJ>uws(Bdvnk)N(+)#(~w}A5om!;@X7s1K)k`)OlEd(=G z#+=|S0g$kLHhs&!!H%`nxw$=6-`6@kou#D09R?JuY{j2K-eFak8eSrSyGwvEGa?gk zy8Ohh|HcYbh4$YBh|7#h6S=j){+2-agd7=^^byzX-%23LoODa`?_d>xq;H^%H%G3Z4|ClF0-J0 zkTDgD!z>`TphZ^7PId8i5eUUab+AM=&K{&{dDGxCOb)QXU&^2H66VYt-usKKUz-!_g&4^NyGHVpzdF}q%U5KC_0d_D%|LB=Bih^$-!yC zaWJZYP-ZbMR)mSY$2e*`nOFi6J}Izr$ud|fMd{VA6N*b3SakBVL5&Ou5DL_fls1%P?XnTC z<%Bh^cK@}Zo{`ecGS4bN9WgOA^)|kXRzn5a;p9f8s&A#lITJUK{*tRb6eXK)9$q)4 z3@O15J>>Qr-D_JAHAz6rhYMv^5e>VaGPE!?*>CHi1(GG!rvy%ZvF_s+Q+x zZ6GRu>e*r%tFglhQ2!TXW@2I@xZbCQ_Dd%5x%mW0rDtnp76fm7M(`RM7fnhweZvP2 zou1~#iVnO=aKPAvir!>$KX_DBEJOL$HXoIheC?av&b03;tNSyq*Y0L&W`;t*$v{df zgsdga|556TsGFuy(m;dG|9}C(-^yIL0%Km=fbo&3eSg4_ZZwqTbB_#qgqiUpW&)<5S+J_jeL~iRlWy=Q zN3MZdL{_HeKu7QH6x>N0?ouh-(}z<(q^``_ z+S-aC;^S3`ga|Zn!Q##Uicw*PmhkV%NP|OG2!p=ZRAX;PNC~!T6VqAr0~?JYFuU1c zl$1Xi;tkrQWI2gK9w6@sf$#TH5% zVsNSeHC#TIL(%{#C;GWE7^e^lY9&lqLKSefE3qPJWAELZrnf0(rz-HMd``8E$@(cDzh};Il*!4051g$##EOKLr?5 zn0?P;*u1KsHB}#o*H+&`|YU~Vf9cHTGwR^(He_;Ere#buMe(- zkOTuI0;2CHENrb19WsA%pv+GiSw>BU4v6W?t2X{f_#X$1yEr?z3x;lt zD1FJTX+MX^tL+pktwVUj8MegJkP-g}jkQ}ZPFlj*RRLxUIS!A(zvLUtvc@;#qrehs z2t&l{0+*S1$mRd0x*(?%K!F+($MU^@te1W3+p6954Je!^0#}6v92_6j z7Yj>H$`s)sArQPdQXFYEfRWr@bWr(IBYhSb*))Z{6%}n-A4f!P(>$xc;vOg)c*lkSlLqI-kX&%c99f$CfQ z6Ef|r54;UCW^=KGnL~lu1EKb$U+*+Wv%DsBz9= zP<E(KZ$mZl@*in-|8Y3O zFN3RPTaAiUoM=^R83WmOTZ6XGFN^j% zIVIIh%WR|F)D^5A3vxFJF|q4j+zKltCOpJkl6aqgo>^(fHydP%=U) z!wS%fa)YG{|C%tZTAA`aA%)y6_~2z@?vd_p|Km$R*$2 zx*>%KzTQu>n@zNwx1HFv9ThJusIyTKr%GkRXz4y7R^#9gd~bx;qO2b`m&H*m8)eK{ z_X_Nk(qK1rbT3+_sEQSLXX83^`2|m5K1#lR8vj8r%0-V|*UrhbYuu-a>%;Xl*1jcLPAsENP^sz{(nE36 zh>x0MZ3NCkFznFef%lpIw*_&A1-#50{a6u{$;tuXaHlmkQW>7qOtd}eA;ANC7#S|m%yk8fATLfAm)1rKZp~g?+$N^ioKCH98qLQ+EsaPc z)S4yMJQ-c39_CnKM3I1y(jpJRW0WFYli3lle?w3|tVXgR)|?w8dN)M zH%qLNQD1A>o8#eO-bqyaNH*7!v~3+bbHA3veh!RjXqVFE878;`vm%jb&{QTD2{ewIQQ*Dv<=6L@MLf~?snm>>O`iC_Fw z+%7a#UqA1k-w(ff+Z_CqZ^EE48tw5u)RKgi!2q=9Q}uIcCaWJRku zOq|s0Ql)f0K4@RQ+Kn;=`6d!hRAOmy(Ky6CDQzE?s9mqwnu#; z`~fM`M)ww*fJ%b%z!gQ}~;)#+jVjmy^O9Pw$I@S!hnt<$lmiP^3$ zWF~hGS&|#M9*W2CGn8-^&=dmNtWAshnLg-|nhc%qb?degn&sE%lx2t7?{)fmK_2h< z&LHtsgU)jq{s_pFk5tq*RoL2JJAYPo=kA0;mCJJS@W;jHwy(KTGW&k zLhc|=ORnLtD1FCFFD+#3t`kEPj(UR^*0MI@F80;S2B2F@!)LbfNS_$#u-a(`umqb# z>p@VT=7-fxm&vHjRN~Tv@9%xIPJ8WUw<-6GO=`%fd5$XtdMlTh4@S{g;{wju&ufe# z4ENd_s7n!$i?a~I#?Y*LhVr~kSv$W|UZsS5v@%wXFXwunF1F#Ic5EojBmpa$#-$O0 z>|NlJ_&({@wYKmn+n&P?Jw#amIi>8@M=iyl;gi(J@Caw7cPsQ0senWth&M7Z&_WUV6axP7H_Y7dKxy*2cd|bcB7j3wEMtOwF{1~5LIRPUoehkxS8rlWsz6}| zAP5VYXNq8!b;*-$9Z~?#yCA<}ct13XAc7_M{~Gk?M=)|$r9)2Wm}M{lY2 zCnx2$4oEF8K1qel0>cR}Ph>!`n}Im_w*>YvzMRE3ay0{?7Jhj(BoLCmYRyXQZ*-Lw zP_G(mkhng?eyPvR;1I`SNKP8Vr>%t8_)b@*Q50;;_$f1m+LOx~^&>QL#ehiRN)VK= zFkMths*~*ZV$>2?@dYd>YAH~6FMpQ@AszE6&}4BOF%YldxF4i|!|H(R__N`a z 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 literal 0 HcmV?d00001 diff --git a/src/think_flow_demo/ZX65~ALHC_7{Q9FKE$X}TQC.jpg b/src/think_flow_demo/ZX65~ALHC_7{Q9FKE$X}TQC.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a2490075dccd761e4e39019515943df0fa7374c7 GIT binary patch literal 90138 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
  • 7@EjLqlVKF!x!Ke}h^5=1EWzO>dy7l-j$e=&LD| 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�UF2eg7sc9 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 literal 0 HcmV?d00001 diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 668b40b8e..172a03872 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -134,6 +134,7 @@ enable = true [experimental] enable_friend_chat = false # 是否启用好友聊天 enable_think_flow = false # 是否启用思维流 注意:可能会消耗大量token,请谨慎开启 +#思维流适合搭配低能耗普通模型使用,例如qwen2.5 32b #下面的模型若使用硅基流动则不需要更改,使用ds官方则改成.env.prod自定义的宏,使用自定义模型则选择定位相似的模型自己填写 #推理模型 From dce1fdd9fd6d88d97a4e76bd8303f7022f48b912 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 27 Mar 2025 17:11:08 +0800 Subject: [PATCH 35/46] =?UTF-8?q?better:=E4=BC=98=E5=8C=96=E8=AE=B0?= =?UTF-8?q?=E5=BF=86=E7=B3=BB=E7=BB=9F=EF=BC=8C=E6=B5=B7=E9=A9=AC=E4=BD=93?= =?UTF-8?q?2.0-10%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/memory_system/Hippocampus.py | 849 +++++++++++++++++++++++ src/plugins/memory_system/config.py | 34 + src/plugins/memory_system/memory.py | 4 - src/think_flow_demo/personality_info.txt | 3 - 4 files changed, 883 insertions(+), 7 deletions(-) create mode 100644 src/plugins/memory_system/Hippocampus.py create mode 100644 src/plugins/memory_system/config.py delete mode 100644 src/think_flow_demo/personality_info.txt diff --git a/src/plugins/memory_system/Hippocampus.py b/src/plugins/memory_system/Hippocampus.py new file mode 100644 index 000000000..67363e95e --- /dev/null +++ b/src/plugins/memory_system/Hippocampus.py @@ -0,0 +1,849 @@ +# -*- coding: utf-8 -*- +import datetime +import math +import random +import time +import re + +import jieba +import networkx as nx + +# from nonebot import get_driver +from ...common.database import db +# from ..chat.config import global_config +from ..chat.utils import ( + calculate_information_content, + cosine_similarity, + get_closest_chat_from_db, + text_to_vector, +) +from ..models.utils_model import LLM_request +from src.common.logger import get_module_logger, LogConfig, MEMORY_STYLE_CONFIG +from src.plugins.memory_system.sample_distribution import MemoryBuildScheduler #分布生成器 +from .config import MemoryConfig + +# 定义日志配置 +memory_config = LogConfig( + # 使用海马体专用样式 + console_format=MEMORY_STYLE_CONFIG["console_format"], + file_format=MEMORY_STYLE_CONFIG["file_format"], +) + + +logger = get_module_logger("memory_system", config=memory_config) + + +class Memory_graph: + def __init__(self): + self.G = nx.Graph() # 使用 networkx 的图结构 + + def connect_dot(self, concept1, concept2): + # 避免自连接 + if concept1 == concept2: + return + + current_time = datetime.datetime.now().timestamp() + + # 如果边已存在,增加 strength + if self.G.has_edge(concept1, concept2): + self.G[concept1][concept2]["strength"] = self.G[concept1][concept2].get("strength", 1) + 1 + # 更新最后修改时间 + self.G[concept1][concept2]["last_modified"] = current_time + else: + # 如果是新边,初始化 strength 为 1 + self.G.add_edge( + concept1, + concept2, + strength=1, + created_time=current_time, # 添加创建时间 + last_modified=current_time, + ) # 添加最后修改时间 + + def add_dot(self, concept, memory): + current_time = datetime.datetime.now().timestamp() + + if concept in self.G: + if "memory_items" in self.G.nodes[concept]: + if not isinstance(self.G.nodes[concept]["memory_items"], list): + self.G.nodes[concept]["memory_items"] = [self.G.nodes[concept]["memory_items"]] + self.G.nodes[concept]["memory_items"].append(memory) + # 更新最后修改时间 + self.G.nodes[concept]["last_modified"] = current_time + else: + self.G.nodes[concept]["memory_items"] = [memory] + # 如果节点存在但没有memory_items,说明是第一次添加memory,设置created_time + if "created_time" not in self.G.nodes[concept]: + self.G.nodes[concept]["created_time"] = current_time + self.G.nodes[concept]["last_modified"] = current_time + else: + # 如果是新节点,创建新的记忆列表 + self.G.add_node( + concept, + memory_items=[memory], + created_time=current_time, # 添加创建时间 + last_modified=current_time, + ) # 添加最后修改时间 + + def get_dot(self, concept): + # 检查节点是否存在于图中 + if concept in self.G: + # 从图中获取节点数据 + node_data = self.G.nodes[concept] + return concept, node_data + return None + + def get_related_item(self, topic, depth=1): + if topic not in self.G: + return [], [] + + first_layer_items = [] + second_layer_items = [] + + # 获取相邻节点 + neighbors = list(self.G.neighbors(topic)) + + # 获取当前节点的记忆项 + node_data = self.get_dot(topic) + if node_data: + concept, data = node_data + if "memory_items" in data: + memory_items = data["memory_items"] + if isinstance(memory_items, list): + first_layer_items.extend(memory_items) + else: + first_layer_items.append(memory_items) + + # 只在depth=2时获取第二层记忆 + if depth >= 2: + # 获取相邻节点的记忆项 + for neighbor in neighbors: + node_data = self.get_dot(neighbor) + if node_data: + concept, data = node_data + if "memory_items" in data: + memory_items = data["memory_items"] + if isinstance(memory_items, list): + second_layer_items.extend(memory_items) + else: + second_layer_items.append(memory_items) + + return first_layer_items, second_layer_items + + @property + def dots(self): + # 返回所有节点对应的 Memory_dot 对象 + return [self.get_dot(node) for node in self.G.nodes()] + + def forget_topic(self, topic): + """随机删除指定话题中的一条记忆,如果话题没有记忆则移除该话题节点""" + if topic not in self.G: + return None + + # 获取话题节点数据 + node_data = self.G.nodes[topic] + + # 如果节点存在memory_items + if "memory_items" in node_data: + memory_items = node_data["memory_items"] + + # 确保memory_items是列表 + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + + # 如果有记忆项可以删除 + if memory_items: + # 随机选择一个记忆项删除 + removed_item = random.choice(memory_items) + memory_items.remove(removed_item) + + # 更新节点的记忆项 + if memory_items: + self.G.nodes[topic]["memory_items"] = memory_items + else: + # 如果没有记忆项了,删除整个节点 + self.G.remove_node(topic) + + return removed_item + + return None + +#负责海马体与其他部分的交互 +class EntorhinalCortex: + def __init__(self, hippocampus): + self.hippocampus = hippocampus + self.memory_graph = hippocampus.memory_graph + self.config = hippocampus.config + + def get_memory_sample(self): + """从数据库获取记忆样本""" + # 硬编码:每条消息最大记忆次数 + max_memorized_time_per_msg = 3 + + # 创建双峰分布的记忆调度器 + scheduler = MemoryBuildScheduler( + n_hours1=self.config.memory_build_distribution[0], + std_hours1=self.config.memory_build_distribution[1], + weight1=self.config.memory_build_distribution[2], + n_hours2=self.config.memory_build_distribution[3], + std_hours2=self.config.memory_build_distribution[4], + weight2=self.config.memory_build_distribution[5], + total_samples=self.config.build_memory_sample_num + ) + + timestamps = scheduler.get_timestamp_array() + logger.info(f"回忆往事: {[time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(ts)) for ts in timestamps]}") + chat_samples = [] + for timestamp in timestamps: + messages = self.random_get_msg_snippet( + timestamp, + self.config.build_memory_sample_length, + max_memorized_time_per_msg + ) + if messages: + time_diff = (datetime.datetime.now().timestamp() - timestamp) / 3600 + logger.debug(f"成功抽取 {time_diff:.1f} 小时前的消息样本,共{len(messages)}条") + chat_samples.append(messages) + else: + logger.debug(f"时间戳 {timestamp} 的消息样本抽取失败") + + return chat_samples + + def random_get_msg_snippet(self, target_timestamp: float, chat_size: int, max_memorized_time_per_msg: int) -> list: + """从数据库中随机获取指定时间戳附近的消息片段""" + try_count = 0 + while try_count < 3: + messages = get_closest_chat_from_db(length=chat_size, timestamp=target_timestamp) + if messages: + for message in messages: + if message["memorized_times"] >= max_memorized_time_per_msg: + messages = None + break + if messages: + for message in messages: + db.messages.update_one( + {"_id": message["_id"]}, {"$set": {"memorized_times": message["memorized_times"] + 1}} + ) + return messages + try_count += 1 + return None + + async def sync_memory_to_db(self): + """将记忆图同步到数据库""" + # 获取数据库中所有节点和内存中所有节点 + db_nodes = list(db.graph_data.nodes.find()) + memory_nodes = list(self.memory_graph.G.nodes(data=True)) + + # 转换数据库节点为字典格式,方便查找 + db_nodes_dict = {node["concept"]: node for node in db_nodes} + + # 检查并更新节点 + for concept, data in memory_nodes: + memory_items = data.get("memory_items", []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + + # 计算内存中节点的特征值 + memory_hash = self.hippocampus.calculate_node_hash(concept, memory_items) + + # 获取时间信息 + created_time = data.get("created_time", datetime.datetime.now().timestamp()) + last_modified = data.get("last_modified", datetime.datetime.now().timestamp()) + + if concept not in db_nodes_dict: + # 数据库中缺少的节点,添加 + node_data = { + "concept": concept, + "memory_items": memory_items, + "hash": memory_hash, + "created_time": created_time, + "last_modified": last_modified, + } + db.graph_data.nodes.insert_one(node_data) + else: + # 获取数据库中节点的特征值 + db_node = db_nodes_dict[concept] + db_hash = db_node.get("hash", None) + + # 如果特征值不同,则更新节点 + if db_hash != memory_hash: + db.graph_data.nodes.update_one( + {"concept": concept}, + { + "$set": { + "memory_items": memory_items, + "hash": memory_hash, + "created_time": created_time, + "last_modified": last_modified, + } + }, + ) + + # 处理边的信息 + db_edges = list(db.graph_data.edges.find()) + memory_edges = list(self.memory_graph.G.edges(data=True)) + + # 创建边的哈希值字典 + db_edge_dict = {} + for edge in db_edges: + edge_hash = self.hippocampus.calculate_edge_hash(edge["source"], edge["target"]) + db_edge_dict[(edge["source"], edge["target"])] = {"hash": edge_hash, "strength": edge.get("strength", 1)} + + # 检查并更新边 + for source, target, data in memory_edges: + edge_hash = self.hippocampus.calculate_edge_hash(source, target) + edge_key = (source, target) + strength = data.get("strength", 1) + + # 获取边的时间信息 + created_time = data.get("created_time", datetime.datetime.now().timestamp()) + last_modified = data.get("last_modified", datetime.datetime.now().timestamp()) + + if edge_key not in db_edge_dict: + # 添加新边 + edge_data = { + "source": source, + "target": target, + "strength": strength, + "hash": edge_hash, + "created_time": created_time, + "last_modified": last_modified, + } + db.graph_data.edges.insert_one(edge_data) + else: + # 检查边的特征值是否变化 + if db_edge_dict[edge_key]["hash"] != edge_hash: + db.graph_data.edges.update_one( + {"source": source, "target": target}, + { + "$set": { + "hash": edge_hash, + "strength": strength, + "created_time": created_time, + "last_modified": last_modified, + } + }, + ) + + def sync_memory_from_db(self): + """从数据库同步数据到内存中的图结构""" + current_time = datetime.datetime.now().timestamp() + need_update = False + + # 清空当前图 + self.memory_graph.G.clear() + + # 从数据库加载所有节点 + nodes = list(db.graph_data.nodes.find()) + for node in nodes: + concept = node["concept"] + memory_items = node.get("memory_items", []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + + # 检查时间字段是否存在 + if "created_time" not in node or "last_modified" not in node: + need_update = True + # 更新数据库中的节点 + update_data = {} + if "created_time" not in node: + update_data["created_time"] = current_time + if "last_modified" not in node: + update_data["last_modified"] = current_time + + db.graph_data.nodes.update_one({"concept": concept}, {"$set": update_data}) + logger.info(f"[时间更新] 节点 {concept} 添加缺失的时间字段") + + # 获取时间信息(如果不存在则使用当前时间) + created_time = node.get("created_time", current_time) + last_modified = node.get("last_modified", current_time) + + # 添加节点到图中 + self.memory_graph.G.add_node( + concept, memory_items=memory_items, created_time=created_time, last_modified=last_modified + ) + + # 从数据库加载所有边 + edges = list(db.graph_data.edges.find()) + for edge in edges: + source = edge["source"] + target = edge["target"] + strength = edge.get("strength", 1) + + # 检查时间字段是否存在 + if "created_time" not in edge or "last_modified" not in edge: + need_update = True + # 更新数据库中的边 + update_data = {} + if "created_time" not in edge: + update_data["created_time"] = current_time + if "last_modified" not in edge: + update_data["last_modified"] = current_time + + db.graph_data.edges.update_one({"source": source, "target": target}, {"$set": update_data}) + logger.info(f"[时间更新] 边 {source} - {target} 添加缺失的时间字段") + + # 获取时间信息(如果不存在则使用当前时间) + created_time = edge.get("created_time", current_time) + last_modified = edge.get("last_modified", current_time) + + # 只有当源节点和目标节点都存在时才添加边 + if source in self.memory_graph.G and target in self.memory_graph.G: + self.memory_graph.G.add_edge( + source, target, strength=strength, created_time=created_time, last_modified=last_modified + ) + + if need_update: + logger.success("[数据库] 已为缺失的时间字段进行补充") + +#负责整合,遗忘,合并记忆 +class ParahippocampalGyrus: + def __init__(self, hippocampus): + self.hippocampus = hippocampus + self.memory_graph = hippocampus.memory_graph + self.config = hippocampus.config + + async def memory_compress(self, messages: list, compress_rate=0.1): + """压缩和总结消息内容,生成记忆主题和摘要。 + + Args: + messages (list): 消息列表,每个消息是一个字典,包含以下字段: + - time: float, 消息的时间戳 + - detailed_plain_text: str, 消息的详细文本内容 + compress_rate (float, optional): 压缩率,用于控制生成的主题数量。默认为0.1。 + + Returns: + tuple: (compressed_memory, similar_topics_dict) + - compressed_memory: set, 压缩后的记忆集合,每个元素是一个元组 (topic, summary) + - topic: str, 记忆主题 + - summary: str, 主题的摘要描述 + - similar_topics_dict: dict, 相似主题字典,key为主题,value为相似主题列表 + 每个相似主题是一个元组 (similar_topic, similarity) + - similar_topic: str, 相似的主题 + - similarity: float, 相似度分数(0-1之间) + + Process: + 1. 合并消息文本并生成时间信息 + 2. 使用LLM提取关键主题 + 3. 过滤掉包含禁用关键词的主题 + 4. 为每个主题生成摘要 + 5. 查找与现有记忆中的相似主题 + """ + if not messages: + return set(), {} + + # 合并消息文本,同时保留时间信息 + input_text = "" + time_info = "" + # 计算最早和最晚时间 + earliest_time = min(msg["time"] for msg in messages) + latest_time = max(msg["time"] for msg in messages) + + earliest_dt = datetime.datetime.fromtimestamp(earliest_time) + latest_dt = datetime.datetime.fromtimestamp(latest_time) + + # 如果是同一年 + if earliest_dt.year == latest_dt.year: + earliest_str = earliest_dt.strftime("%m-%d %H:%M:%S") + latest_str = latest_dt.strftime("%m-%d %H:%M:%S") + time_info += f"是在{earliest_dt.year}年,{earliest_str} 到 {latest_str} 的对话:\n" + else: + earliest_str = earliest_dt.strftime("%Y-%m-%d %H:%M:%S") + latest_str = latest_dt.strftime("%Y-%m-%d %H:%M:%S") + time_info += f"是从 {earliest_str} 到 {latest_str} 的对话:\n" + + for msg in messages: + input_text += f"{msg['detailed_plain_text']}\n" + + logger.debug(input_text) + + topic_num = self.hippocampus.calculate_topic_num(input_text, compress_rate) + topics_response = await self.hippocampus.llm_topic_judge.generate_response(self.hippocampus.find_topic_llm(input_text, topic_num)) + + # 使用正则表达式提取<>中的内容 + topics = re.findall(r'<([^>]+)>', topics_response[0]) + + # 如果没有找到<>包裹的内容,返回['none'] + if not topics: + topics = ['none'] + else: + # 处理提取出的话题 + topics = [ + topic.strip() + for topic in ','.join(topics).replace(",", ",").replace("、", ",").replace(" ", ",").split(",") + if topic.strip() + ] + + # 过滤掉包含禁用关键词的topic + filtered_topics = [ + topic for topic in topics + if not any(keyword in topic for keyword in self.config.memory_ban_words) + ] + + logger.debug(f"过滤后话题: {filtered_topics}") + + # 创建所有话题的请求任务 + tasks = [] + for topic in filtered_topics: + topic_what_prompt = self.hippocampus.topic_what(input_text, topic, time_info) + task = self.hippocampus.llm_summary_by_topic.generate_response_async(topic_what_prompt) + tasks.append((topic.strip(), task)) + + # 等待所有任务完成 + compressed_memory = set() + similar_topics_dict = {} + + for topic, task in tasks: + response = await task + if response: + compressed_memory.add((topic, response[0])) + + existing_topics = list(self.memory_graph.G.nodes()) + similar_topics = [] + + for existing_topic in existing_topics: + topic_words = set(jieba.cut(topic)) + existing_words = set(jieba.cut(existing_topic)) + + all_words = topic_words | existing_words + v1 = [1 if word in topic_words else 0 for word in all_words] + v2 = [1 if word in existing_words else 0 for word in all_words] + + similarity = cosine_similarity(v1, v2) + + if similarity >= 0.7: + similar_topics.append((existing_topic, similarity)) + + similar_topics.sort(key=lambda x: x[1], reverse=True) + similar_topics = similar_topics[:3] + similar_topics_dict[topic] = similar_topics + + return compressed_memory, similar_topics_dict + + async def operation_build_memory(self): + logger.debug("------------------------------------开始构建记忆--------------------------------------") + start_time = time.time() + memory_samples = self.hippocampus.entorhinal_cortex.get_memory_sample() + all_added_nodes = [] + all_connected_nodes = [] + all_added_edges = [] + for i, messages in enumerate(memory_samples, 1): + all_topics = [] + progress = (i / len(memory_samples)) * 100 + bar_length = 30 + filled_length = int(bar_length * i // len(memory_samples)) + bar = "█" * filled_length + "-" * (bar_length - filled_length) + logger.debug(f"进度: [{bar}] {progress:.1f}% ({i}/{len(memory_samples)})") + + compress_rate = self.config.memory_compress_rate + compressed_memory, similar_topics_dict = await self.memory_compress(messages, compress_rate) + logger.debug(f"压缩后记忆数量: {compressed_memory},似曾相识的话题: {similar_topics_dict}") + + current_time = datetime.datetime.now().timestamp() + logger.debug(f"添加节点: {', '.join(topic for topic, _ in compressed_memory)}") + all_added_nodes.extend(topic for topic, _ in compressed_memory) + + for topic, memory in compressed_memory: + self.memory_graph.add_dot(topic, memory) + all_topics.append(topic) + + if topic in similar_topics_dict: + similar_topics = similar_topics_dict[topic] + for similar_topic, similarity in similar_topics: + if topic != similar_topic: + strength = int(similarity * 10) + + logger.debug(f"连接相似节点: {topic} 和 {similar_topic} (强度: {strength})") + all_added_edges.append(f"{topic}-{similar_topic}") + + all_connected_nodes.append(topic) + all_connected_nodes.append(similar_topic) + + self.memory_graph.G.add_edge( + topic, + similar_topic, + strength=strength, + created_time=current_time, + last_modified=current_time, + ) + + for i in range(len(all_topics)): + for j in range(i + 1, len(all_topics)): + logger.debug(f"连接同批次节点: {all_topics[i]} 和 {all_topics[j]}") + all_added_edges.append(f"{all_topics[i]}-{all_topics[j]}") + self.memory_graph.connect_dot(all_topics[i], all_topics[j]) + + logger.success(f"更新记忆: {', '.join(all_added_nodes)}") + logger.debug(f"强化连接: {', '.join(all_added_edges)}") + logger.info(f"强化连接节点: {', '.join(all_connected_nodes)}") + + await self.hippocampus.entorhinal_cortex.sync_memory_to_db() + + end_time = time.time() + logger.success( + f"---------------------记忆构建耗时: {end_time - start_time:.2f} " + "秒---------------------" + ) + + async def operation_forget_topic(self, percentage=0.1): + logger.info("[遗忘] 开始检查数据库...") + + all_nodes = list(self.memory_graph.G.nodes()) + all_edges = list(self.memory_graph.G.edges()) + + if not all_nodes and not all_edges: + logger.info("[遗忘] 记忆图为空,无需进行遗忘操作") + return + + check_nodes_count = max(1, int(len(all_nodes) * percentage)) + check_edges_count = max(1, int(len(all_edges) * percentage)) + + nodes_to_check = random.sample(all_nodes, check_nodes_count) + edges_to_check = random.sample(all_edges, check_edges_count) + + edge_changes = {"weakened": 0, "removed": 0} + node_changes = {"reduced": 0, "removed": 0} + + current_time = datetime.datetime.now().timestamp() + + logger.info("[遗忘] 开始检查连接...") + for source, target in edges_to_check: + edge_data = self.memory_graph.G[source][target] + last_modified = edge_data.get("last_modified") + + if current_time - last_modified > 3600 * self.config.memory_forget_time: + current_strength = edge_data.get("strength", 1) + new_strength = current_strength - 1 + + if new_strength <= 0: + self.memory_graph.G.remove_edge(source, target) + edge_changes["removed"] += 1 + logger.info(f"[遗忘] 连接移除: {source} -> {target}") + else: + edge_data["strength"] = new_strength + edge_data["last_modified"] = current_time + edge_changes["weakened"] += 1 + logger.info(f"[遗忘] 连接减弱: {source} -> {target} (强度: {current_strength} -> {new_strength})") + + logger.info("[遗忘] 开始检查节点...") + for node in nodes_to_check: + node_data = self.memory_graph.G.nodes[node] + last_modified = node_data.get("last_modified", current_time) + + if current_time - last_modified > 3600 * 24: + memory_items = node_data.get("memory_items", []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + + if memory_items: + current_count = len(memory_items) + removed_item = random.choice(memory_items) + memory_items.remove(removed_item) + + if memory_items: + self.memory_graph.G.nodes[node]["memory_items"] = memory_items + self.memory_graph.G.nodes[node]["last_modified"] = current_time + node_changes["reduced"] += 1 + logger.info(f"[遗忘] 记忆减少: {node} (数量: {current_count} -> {len(memory_items)})") + else: + self.memory_graph.G.remove_node(node) + node_changes["removed"] += 1 + logger.info(f"[遗忘] 节点移除: {node}") + + if any(count > 0 for count in edge_changes.values()) or any(count > 0 for count in node_changes.values()): + await self.hippocampus.entorhinal_cortex.sync_memory_to_db() + logger.info("[遗忘] 统计信息:") + logger.info(f"[遗忘] 连接变化: {edge_changes['weakened']} 个减弱, {edge_changes['removed']} 个移除") + logger.info(f"[遗忘] 节点变化: {node_changes['reduced']} 个减少记忆, {node_changes['removed']} 个移除") + else: + logger.info("[遗忘] 本次检查没有节点或连接满足遗忘条件") + +# 海马体 +class Hippocampus: + def __init__(self): + self.memory_graph = Memory_graph() + self.llm_topic_judge = None + self.llm_summary_by_topic = None + self.entorhinal_cortex = None + self.parahippocampal_gyrus = None + self.config = None + + def initialize(self, global_config): + self.config = MemoryConfig.from_global_config(global_config) + # 初始化子组件 + self.entorhinal_cortex = EntorhinalCortex(self) + self.parahippocampal_gyrus = ParahippocampalGyrus(self) + # 从数据库加载记忆图 + self.entorhinal_cortex.sync_memory_from_db() + self.llm_topic_judge = self.config.llm_topic_judge + self.llm_summary_by_topic = self.config.llm_summary_by_topic + + def get_all_node_names(self) -> list: + """获取记忆图中所有节点的名字列表""" + return list(self.memory_graph.G.nodes()) + + def calculate_node_hash(self, concept, memory_items) -> int: + """计算节点的特征值""" + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + sorted_items = sorted(memory_items) + content = f"{concept}:{'|'.join(sorted_items)}" + return hash(content) + + def calculate_edge_hash(self, source, target) -> int: + """计算边的特征值""" + nodes = sorted([source, target]) + return hash(f"{nodes[0]}:{nodes[1]}") + + def find_topic_llm(self, text, topic_num): + prompt = ( + f"这是一段文字:{text}。请你从这段话中总结出最多{topic_num}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来," + f"将主题用逗号隔开,并加上<>,例如<主题1>,<主题2>......尽可能精简。只需要列举最多{topic_num}个话题就好,不要有序号,不要告诉我其他内容。" + f"如果找不出主题或者没有明显主题,返回。" + ) + return prompt + + def topic_what(self, text, topic, time_info): + prompt = ( + f'这是一段文字,{time_info}:{text}。我想让你基于这段文字来概括"{topic}"这个概念,帮我总结成一句自然的话,' + f"可以包含时间和人物,以及具体的观点。只输出这句话就好" + ) + return prompt + + def calculate_topic_num(self, text, compress_rate): + """计算文本的话题数量""" + information_content = calculate_information_content(text) + topic_by_length = text.count("\n") * compress_rate + topic_by_information_content = max(1, min(5, int((information_content - 3) * 2))) + topic_num = int((topic_by_length + topic_by_information_content) / 2) + logger.debug( + f"topic_by_length: {topic_by_length}, topic_by_information_content: {topic_by_information_content}, " + f"topic_num: {topic_num}" + ) + return topic_num + + def get_memory_from_keyword(self, keyword: str, max_depth: int = 2) -> list: + """从关键词获取相关记忆。 + + Args: + keyword (str): 关键词 + max_depth (int, optional): 记忆检索深度,默认为2。1表示只获取直接相关的记忆,2表示获取间接相关的记忆。 + + Returns: + list: 记忆列表,每个元素是一个元组 (topic, memory_items, similarity) + - topic: str, 记忆主题 + - memory_items: list, 该主题下的记忆项列表 + - similarity: float, 与关键词的相似度 + """ + if not keyword: + return [] + + # 获取所有节点 + all_nodes = list(self.memory_graph.G.nodes()) + memories = [] + + # 计算关键词的词集合 + keyword_words = set(jieba.cut(keyword)) + + # 遍历所有节点,计算相似度 + for node in all_nodes: + node_words = set(jieba.cut(node)) + all_words = keyword_words | node_words + v1 = [1 if word in keyword_words else 0 for word in all_words] + v2 = [1 if word in node_words else 0 for word in all_words] + similarity = cosine_similarity(v1, v2) + + # 如果相似度超过阈值,获取该节点的记忆 + if similarity >= 0.3: # 可以调整这个阈值 + node_data = self.memory_graph.G.nodes[node] + memory_items = node_data.get("memory_items", []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + + memories.append((node, memory_items, similarity)) + + # 按相似度降序排序 + memories.sort(key=lambda x: x[2], reverse=True) + return memories + + async def get_memory_from_text(self, text: str, num: int = 5, max_depth: int = 2, + fast_retrieval: bool = False) -> list: + """从文本中提取关键词并获取相关记忆。 + + Args: + text (str): 输入文本 + num (int, optional): 需要返回的记忆数量。默认为5。 + max_depth (int, optional): 记忆检索深度。默认为2。 + fast_retrieval (bool, optional): 是否使用快速检索。默认为False。 + 如果为True,使用jieba分词和TF-IDF提取关键词,速度更快但可能不够准确。 + 如果为False,使用LLM提取关键词,速度较慢但更准确。 + + Returns: + list: 记忆列表,每个元素是一个元组 (topic, memory_items, similarity) + - topic: str, 记忆主题 + - memory_items: list, 该主题下的记忆项列表 + - similarity: float, 与文本的相似度 + """ + if not text: + return [] + + if fast_retrieval: + # 使用jieba分词提取关键词 + words = jieba.cut(text) + # 过滤掉停用词和单字词 + keywords = [word for word in words if len(word) > 1] + # 去重 + keywords = list(set(keywords)) + # 限制关键词数量 + keywords = keywords[:5] + else: + # 使用LLM提取关键词 + topic_num = min(5, max(1, int(len(text) * 0.1))) # 根据文本长度动态调整关键词数量 + topics_response = await self.llm_topic_judge.generate_response( + self.find_topic_llm(text, topic_num) + ) + + # 提取关键词 + keywords = re.findall(r'<([^>]+)>', topics_response[0]) + if not keywords: + keywords = ['none'] + else: + keywords = [ + keyword.strip() + for keyword in ','.join(keywords).replace(",", ",").replace("、", ",").replace(" ", ",").split(",") + if keyword.strip() + ] + + # 从每个关键词获取记忆 + all_memories = [] + for keyword in keywords: + memories = self.get_memory_from_keyword(keyword, max_depth) + all_memories.extend(memories) + + # 去重(基于主题) + seen_topics = set() + unique_memories = [] + for topic, memory_items, similarity in all_memories: + if topic not in seen_topics: + seen_topics.add(topic) + unique_memories.append((topic, memory_items, similarity)) + + # 按相似度排序并返回前num个 + unique_memories.sort(key=lambda x: x[2], reverse=True) + return unique_memories[:num] + +# driver = get_driver() +# config = driver.config + +start_time = time.time() + +# 创建记忆图 +memory_graph = Memory_graph() +# 创建海马体 +hippocampus = Hippocampus() + +# 从全局配置初始化记忆系统 +from ..chat.config import global_config +hippocampus.initialize(global_config=global_config) + +end_time = time.time() +logger.success(f"加载海马体耗时: {end_time - start_time:.2f} 秒") diff --git a/src/plugins/memory_system/config.py b/src/plugins/memory_system/config.py new file mode 100644 index 000000000..fe688372f --- /dev/null +++ b/src/plugins/memory_system/config.py @@ -0,0 +1,34 @@ +from dataclasses import dataclass +from typing import List + +@dataclass +class MemoryConfig: + """记忆系统配置类""" + # 记忆构建相关配置 + memory_build_distribution: List[float] # 记忆构建的时间分布参数 + build_memory_sample_num: int # 每次构建记忆的样本数量 + build_memory_sample_length: int # 每个样本的消息长度 + memory_compress_rate: float # 记忆压缩率 + + # 记忆遗忘相关配置 + memory_forget_time: int # 记忆遗忘时间(小时) + + # 记忆过滤相关配置 + memory_ban_words: List[str] # 记忆过滤词列表 + + llm_topic_judge: str # 话题判断模型 + llm_summary_by_topic: str # 话题总结模型 + + @classmethod + def from_global_config(cls, global_config): + """从全局配置创建记忆系统配置""" + return cls( + memory_build_distribution=global_config.memory_build_distribution, + build_memory_sample_num=global_config.build_memory_sample_num, + build_memory_sample_length=global_config.build_memory_sample_length, + memory_compress_rate=global_config.memory_compress_rate, + memory_forget_time=global_config.memory_forget_time, + memory_ban_words=global_config.memory_ban_words, + llm_topic_judge=global_config.topic_judge_model, + llm_summary_by_topic=global_config.summary_by_topic_model + ) \ No newline at end of file diff --git a/src/plugins/memory_system/memory.py b/src/plugins/memory_system/memory.py index c2cdb73e6..e0151c04c 100644 --- a/src/plugins/memory_system/memory.py +++ b/src/plugins/memory_system/memory.py @@ -27,10 +27,6 @@ memory_config = LogConfig( console_format=MEMORY_STYLE_CONFIG["console_format"], file_format=MEMORY_STYLE_CONFIG["file_format"], ) -# print(f"memory_config: {memory_config}") -# print(f"MEMORY_STYLE_CONFIG: {MEMORY_STYLE_CONFIG}") -# print(f"MEMORY_STYLE_CONFIG['console_format']: {MEMORY_STYLE_CONFIG['console_format']}") -# print(f"MEMORY_STYLE_CONFIG['file_format']: {MEMORY_STYLE_CONFIG['file_format']}") logger = get_module_logger("memory_system", config=memory_config) diff --git a/src/think_flow_demo/personality_info.txt b/src/think_flow_demo/personality_info.txt deleted file mode 100644 index d7b9e4ecf..000000000 --- a/src/think_flow_demo/personality_info.txt +++ /dev/null @@ -1,3 +0,0 @@ -// 为了解决issue-589,已经将心流引用的内容改为了bot_config.toml中的prompt_personality -// 请移步配置文件进行更改 -你叫麦麦,你19岁,是一个大二的女大学生,你有一头黑色短发,你会刷贴吧 \ No newline at end of file From 11e5a694480ee71c57c512bab45737c6acde7907 Mon Sep 17 00:00:00 2001 From: DrSmoothl <1787882683@qq.com> Date: Thu, 27 Mar 2025 18:58:59 +0800 Subject: [PATCH 36/46] =?UTF-8?q?=E6=9B=B4=E6=96=B0WebUI=E4=BB=A5=E9=80=82?= =?UTF-8?q?=E9=85=8D=E6=9C=80=E6=96=B0=E7=89=88config=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/bot_config_template.toml | 2 +- webui.py | 640 +++++++++++++++++++++++++----- 2 files changed, 533 insertions(+), 109 deletions(-) diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 172a03872..b64e79f2c 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -84,7 +84,7 @@ check_prompt = "符合公序良俗" # 表情包过滤要求 [memory] build_memory_interval = 2000 # 记忆构建间隔 单位秒 间隔越低,麦麦学习越多,但是冗余信息也会增多 -build_memory_distribution = [4,2,0.6,24,8,0.4] # 记忆构建分布,参数:分布1均值,标准差,权重,分布2均值,标准差,权重 +build_memory_distribution = [4.0,2.0,0.6,24.0,8.0,0.4] # 记忆构建分布,参数:分布1均值,标准差,权重,分布2均值,标准差,权重 build_memory_sample_num = 10 # 采样数量,数值越高记忆采样次数越多 build_memory_sample_length = 20 # 采样长度,数值越高一段记忆内容越丰富 memory_compress_rate = 0.1 # 记忆压缩率 控制记忆精简程度 建议保持默认,调高可以获得更多信息,但是冗余信息也会增多 diff --git a/webui.py b/webui.py index 85c1115d0..d45259dcc 100644 --- a/webui.py +++ b/webui.py @@ -5,6 +5,7 @@ import toml import signal import sys import requests +import socket try: from src.common.logger import get_module_logger @@ -39,50 +40,35 @@ def signal_handler(signum, frame): signal.signal(signal.SIGINT, signal_handler) is_share = False -debug = True -# 检查配置文件是否存在 -if not os.path.exists("config/bot_config.toml"): - logger.error("配置文件 bot_config.toml 不存在,请检查配置文件路径") - raise FileNotFoundError("配置文件 bot_config.toml 不存在,请检查配置文件路径") - -if not os.path.exists(".env.prod"): - logger.error("环境配置文件 .env.prod 不存在,请检查配置文件路径") - raise FileNotFoundError("环境配置文件 .env.prod 不存在,请检查配置文件路径") - -config_data = toml.load("config/bot_config.toml") -# 增加对老版本配置文件支持 -LEGACY_CONFIG_VERSION = version.parse("0.0.1") - -# 增加最低支持版本 -MIN_SUPPORT_VERSION = version.parse("0.0.8") -MIN_SUPPORT_MAIMAI_VERSION = version.parse("0.5.13") - -if "inner" in config_data: - CONFIG_VERSION = config_data["inner"]["version"] - PARSED_CONFIG_VERSION = version.parse(CONFIG_VERSION) - if PARSED_CONFIG_VERSION < MIN_SUPPORT_VERSION: - logger.error("您的麦麦版本过低!!已经不再支持,请更新到最新版本!!") - logger.error("最低支持的麦麦版本:" + str(MIN_SUPPORT_MAIMAI_VERSION)) - raise Exception("您的麦麦版本过低!!已经不再支持,请更新到最新版本!!") -else: - logger.error("您的麦麦版本过低!!已经不再支持,请更新到最新版本!!") - logger.error("最低支持的麦麦版本:" + str(MIN_SUPPORT_MAIMAI_VERSION)) - raise Exception("您的麦麦版本过低!!已经不再支持,请更新到最新版本!!") - - -HAVE_ONLINE_STATUS_VERSION = version.parse("0.0.9") - -# 定义意愿模式可选项 -WILLING_MODE_CHOICES = [ - "classical", - "dynamic", - "custom", -] - - -# 添加WebUI配置文件版本 -WEBUI_VERSION = version.parse("0.0.10") +debug = False +def init_model_pricing(): + """初始化模型价格配置""" + model_list = [ + "llm_reasoning", + "llm_reasoning_minor", + "llm_normal", + "llm_topic_judge", + "llm_summary_by_topic", + "llm_emotion_judge", + "vlm", + "embedding", + "moderation" + ] + + for model in model_list: + if model in config_data["model"]: + # 检查是否已有pri_in和pri_out配置 + has_pri_in = "pri_in" in config_data["model"][model] + has_pri_out = "pri_out" in config_data["model"][model] + + # 只在缺少配置时添加默认值 + if not has_pri_in: + config_data["model"][model]["pri_in"] = 0 + logger.info(f"为模型 {model} 添加默认输入价格配置") + if not has_pri_out: + config_data["model"][model]["pri_out"] = 0 + logger.info(f"为模型 {model} 添加默认输出价格配置") # ============================================== # env环境配置文件读取部分 @@ -124,6 +110,68 @@ def parse_env_config(config_file): return env_variables +# 检查配置文件是否存在 +if not os.path.exists("config/bot_config.toml"): + logger.error("配置文件 bot_config.toml 不存在,请检查配置文件路径") + raise FileNotFoundError("配置文件 bot_config.toml 不存在,请检查配置文件路径") +else: + config_data = toml.load("config/bot_config.toml") + init_model_pricing() + +if not os.path.exists(".env.prod"): + logger.error("环境配置文件 .env.prod 不存在,请检查配置文件路径") + raise FileNotFoundError("环境配置文件 .env.prod 不存在,请检查配置文件路径") +else: + # 载入env文件并解析 + env_config_file = ".env.prod" # 配置文件路径 + env_config_data = parse_env_config(env_config_file) + +# 增加最低支持版本 +MIN_SUPPORT_VERSION = version.parse("0.0.8") +MIN_SUPPORT_MAIMAI_VERSION = version.parse("0.5.13") + +if "inner" in config_data: + CONFIG_VERSION = config_data["inner"]["version"] + PARSED_CONFIG_VERSION = version.parse(CONFIG_VERSION) + if PARSED_CONFIG_VERSION < MIN_SUPPORT_VERSION: + logger.error("您的麦麦版本过低!!已经不再支持,请更新到最新版本!!") + logger.error("最低支持的麦麦版本:" + str(MIN_SUPPORT_MAIMAI_VERSION)) + raise Exception("您的麦麦版本过低!!已经不再支持,请更新到最新版本!!") +else: + logger.error("您的麦麦版本过低!!已经不再支持,请更新到最新版本!!") + logger.error("最低支持的麦麦版本:" + str(MIN_SUPPORT_MAIMAI_VERSION)) + raise Exception("您的麦麦版本过低!!已经不再支持,请更新到最新版本!!") + +# 添加麦麦版本 + +if "mai_version" in config_data: + MAI_VERSION = version.parse(str(config_data["mai_version"]["version"])) + logger.info("您的麦麦版本为:" + str(MAI_VERSION)) +else: + logger.info("检测到配置文件中并没有定义麦麦版本,将使用默认版本") + MAI_VERSION = version.parse("0.5.15") + logger.info("您的麦麦版本为:" + str(MAI_VERSION)) + +# 增加在线状态更新版本 +HAVE_ONLINE_STATUS_VERSION = version.parse("0.0.9") +# 增加日程设置重构版本 +SCHEDULE_CHANGED_VERSION = version.parse("0.0.11") + +# 定义意愿模式可选项 +WILLING_MODE_CHOICES = [ + "classical", + "dynamic", + "custom", +] + + +# 添加WebUI配置文件版本 +WEBUI_VERSION = version.parse("0.0.11") + + + + + # env环境配置文件保存函数 def save_to_env_file(env_variables, filename=".env.prod"): """ @@ -482,7 +530,9 @@ def save_personality_config( t_prompt_personality_1, t_prompt_personality_2, t_prompt_personality_3, - t_prompt_schedule, + t_enable_schedule_gen, + t_prompt_schedule_gen, + t_schedule_doing_update_interval, t_personality_1_probability, t_personality_2_probability, t_personality_3_probability, @@ -492,8 +542,13 @@ def save_personality_config( config_data["personality"]["prompt_personality"][1] = t_prompt_personality_2 config_data["personality"]["prompt_personality"][2] = t_prompt_personality_3 - # 保存日程生成提示词 - config_data["personality"]["prompt_schedule"] = t_prompt_schedule + # 保存日程生成部分 + if PARSED_CONFIG_VERSION >= SCHEDULE_CHANGED_VERSION: + config_data["schedule"]["enable_schedule_gen"] = t_enable_schedule_gen + config_data["schedule"]["prompt_schedule_gen"] = t_prompt_schedule_gen + config_data["schedule"]["schedule_doing_update_interval"] = t_schedule_doing_update_interval + else: + config_data["personality"]["prompt_schedule"] = t_prompt_schedule_gen # 保存三个人格的概率 config_data["personality"]["personality_1_probability"] = t_personality_1_probability @@ -521,13 +576,15 @@ def save_message_and_emoji_config( t_enable_check, t_check_prompt, ): - config_data["message"]["min_text_length"] = t_min_text_length + if PARSED_CONFIG_VERSION < version.parse("0.0.11"): + config_data["message"]["min_text_length"] = t_min_text_length config_data["message"]["max_context_size"] = t_max_context_size config_data["message"]["emoji_chance"] = t_emoji_chance config_data["message"]["thinking_timeout"] = t_thinking_timeout - config_data["message"]["response_willing_amplifier"] = t_response_willing_amplifier - config_data["message"]["response_interested_rate_amplifier"] = t_response_interested_rate_amplifier - config_data["message"]["down_frequency_rate"] = t_down_frequency_rate + if PARSED_CONFIG_VERSION < version.parse("0.0.11"): + config_data["message"]["response_willing_amplifier"] = t_response_willing_amplifier + config_data["message"]["response_interested_rate_amplifier"] = t_response_interested_rate_amplifier + config_data["message"]["down_frequency_rate"] = t_down_frequency_rate config_data["message"]["ban_words"] = t_ban_words_final_result config_data["message"]["ban_msgs_regex"] = t_ban_msgs_regex_final_result config_data["emoji"]["check_interval"] = t_check_interval @@ -539,6 +596,21 @@ def save_message_and_emoji_config( logger.info("消息和表情配置已保存到 bot_config.toml 文件中") return "消息和表情配置已保存" +def save_willing_config( + t_willing_mode, + t_response_willing_amplifier, + t_response_interested_rate_amplifier, + t_down_frequency_rate, + t_emoji_response_penalty, +): + config_data["willing"]["willing_mode"] = t_willing_mode + config_data["willing"]["response_willing_amplifier"] = t_response_willing_amplifier + config_data["willing"]["response_interested_rate_amplifier"] = t_response_interested_rate_amplifier + config_data["willing"]["down_frequency_rate"] = t_down_frequency_rate + config_data["willing"]["emoji_response_penalty"] = t_emoji_response_penalty + save_config_to_file(config_data) + logger.info("willinng配置已保存到 bot_config.toml 文件中") + return "willinng配置已保存" def save_response_model_config( t_willing_mode, @@ -552,39 +624,79 @@ def save_response_model_config( t_model1_pri_out, t_model2_name, t_model2_provider, + t_model2_pri_in, + t_model2_pri_out, t_model3_name, t_model3_provider, + t_model3_pri_in, + t_model3_pri_out, t_emotion_model_name, t_emotion_model_provider, + t_emotion_model_pri_in, + t_emotion_model_pri_out, t_topic_judge_model_name, t_topic_judge_model_provider, + t_topic_judge_model_pri_in, + t_topic_judge_model_pri_out, t_summary_by_topic_model_name, t_summary_by_topic_model_provider, + t_summary_by_topic_model_pri_in, + t_summary_by_topic_model_pri_out, t_vlm_model_name, t_vlm_model_provider, + t_vlm_model_pri_in, + t_vlm_model_pri_out, ): if PARSED_CONFIG_VERSION >= version.parse("0.0.10"): config_data["willing"]["willing_mode"] = t_willing_mode config_data["response"]["model_r1_probability"] = t_model_r1_probability config_data["response"]["model_v3_probability"] = t_model_r2_probability config_data["response"]["model_r1_distill_probability"] = t_model_r3_probability - config_data["response"]["max_response_length"] = t_max_response_length + if PARSED_CONFIG_VERSION <= version.parse("0.0.10"): + config_data["response"]["max_response_length"] = t_max_response_length + + # 保存模型1配置 config_data["model"]["llm_reasoning"]["name"] = t_model1_name config_data["model"]["llm_reasoning"]["provider"] = t_model1_provider config_data["model"]["llm_reasoning"]["pri_in"] = t_model1_pri_in config_data["model"]["llm_reasoning"]["pri_out"] = t_model1_pri_out + + # 保存模型2配置 config_data["model"]["llm_normal"]["name"] = t_model2_name config_data["model"]["llm_normal"]["provider"] = t_model2_provider + config_data["model"]["llm_normal"]["pri_in"] = t_model2_pri_in + config_data["model"]["llm_normal"]["pri_out"] = t_model2_pri_out + + # 保存模型3配置 config_data["model"]["llm_reasoning_minor"]["name"] = t_model3_name - config_data["model"]["llm_normal"]["provider"] = t_model3_provider + config_data["model"]["llm_reasoning_minor"]["provider"] = t_model3_provider + config_data["model"]["llm_reasoning_minor"]["pri_in"] = t_model3_pri_in + config_data["model"]["llm_reasoning_minor"]["pri_out"] = t_model3_pri_out + + # 保存情感模型配置 config_data["model"]["llm_emotion_judge"]["name"] = t_emotion_model_name config_data["model"]["llm_emotion_judge"]["provider"] = t_emotion_model_provider + config_data["model"]["llm_emotion_judge"]["pri_in"] = t_emotion_model_pri_in + config_data["model"]["llm_emotion_judge"]["pri_out"] = t_emotion_model_pri_out + + # 保存主题判断模型配置 config_data["model"]["llm_topic_judge"]["name"] = t_topic_judge_model_name config_data["model"]["llm_topic_judge"]["provider"] = t_topic_judge_model_provider + config_data["model"]["llm_topic_judge"]["pri_in"] = t_topic_judge_model_pri_in + config_data["model"]["llm_topic_judge"]["pri_out"] = t_topic_judge_model_pri_out + + # 保存主题总结模型配置 config_data["model"]["llm_summary_by_topic"]["name"] = t_summary_by_topic_model_name config_data["model"]["llm_summary_by_topic"]["provider"] = t_summary_by_topic_model_provider + config_data["model"]["llm_summary_by_topic"]["pri_in"] = t_summary_by_topic_model_pri_in + config_data["model"]["llm_summary_by_topic"]["pri_out"] = t_summary_by_topic_model_pri_out + + # 保存识图模型配置 config_data["model"]["vlm"]["name"] = t_vlm_model_name config_data["model"]["vlm"]["provider"] = t_vlm_model_provider + config_data["model"]["vlm"]["pri_in"] = t_vlm_model_pri_in + config_data["model"]["vlm"]["pri_out"] = t_vlm_model_pri_out + save_config_to_file(config_data) logger.info("回复&模型设置已保存到 bot_config.toml 文件中") return "回复&模型设置已保存" @@ -600,6 +712,12 @@ def save_memory_mood_config( t_mood_update_interval, t_mood_decay_rate, t_mood_intensity_factor, + t_build_memory_dist1_mean, + t_build_memory_dist1_std, + t_build_memory_dist1_weight, + t_build_memory_dist2_mean, + t_build_memory_dist2_std, + t_build_memory_dist2_weight, ): config_data["memory"]["build_memory_interval"] = t_build_memory_interval config_data["memory"]["memory_compress_rate"] = t_memory_compress_rate @@ -607,6 +725,15 @@ def save_memory_mood_config( config_data["memory"]["memory_forget_time"] = t_memory_forget_time config_data["memory"]["memory_forget_percentage"] = t_memory_forget_percentage config_data["memory"]["memory_ban_words"] = t_memory_ban_words_final_result + if PARSED_CONFIG_VERSION >= version.parse("0.0.11"): + config_data["memory"]["build_memory_distribution"] = [ + t_build_memory_dist1_mean, + t_build_memory_dist1_std, + t_build_memory_dist1_weight, + t_build_memory_dist2_mean, + t_build_memory_dist2_std, + t_build_memory_dist2_weight, + ] config_data["mood"]["update_interval"] = t_mood_update_interval config_data["mood"]["decay_rate"] = t_mood_decay_rate config_data["mood"]["intensity_factor"] = t_mood_intensity_factor @@ -627,6 +754,9 @@ def save_other_config( t_tone_error_rate, t_word_replace_rate, t_remote_status, + t_enable_response_spliter, + t_max_response_length, + t_max_sentence_num, ): config_data["keywords_reaction"]["enable"] = t_keywords_reaction_enabled config_data["others"]["enable_advance_output"] = t_enable_advance_output @@ -640,6 +770,10 @@ def save_other_config( config_data["chinese_typo"]["word_replace_rate"] = t_word_replace_rate if PARSED_CONFIG_VERSION > HAVE_ONLINE_STATUS_VERSION: config_data["remote"]["enable"] = t_remote_status + if PARSED_CONFIG_VERSION >= version.parse("0.0.11"): + config_data["response_spliter"]["enable_response_spliter"] = t_enable_response_spliter + config_data["response_spliter"]["response_max_length"] = t_max_response_length + config_data["response_spliter"]["response_max_sentence_num"] = t_max_sentence_num save_config_to_file(config_data) logger.info("其他设置已保存到 bot_config.toml 文件中") return "其他设置已保存" @@ -657,7 +791,6 @@ def save_group_config( logger.info("群聊设置已保存到 bot_config.toml 文件中") return "群聊设置已保存" - with gr.Blocks(title="MaimBot配置文件编辑") as app: gr.Markdown( value=""" @@ -997,11 +1130,32 @@ with gr.Blocks(title="MaimBot配置文件编辑") as app: inputs=personality_probability_change_inputs, outputs=[warning_less_text], ) - with gr.Row(): - prompt_schedule = gr.Textbox( - label="日程生成提示词", value=config_data["personality"]["prompt_schedule"], interactive=True - ) + gr.Markdown("---") + with gr.Row(): + gr.Markdown("麦麦提示词设置") + if PARSED_CONFIG_VERSION >= SCHEDULE_CHANGED_VERSION: + with gr.Row(): + enable_schedule_gen = gr.Checkbox(value=config_data["schedule"]["enable_schedule_gen"], + label="是否开启麦麦日程生成(尚未完成)", + interactive=True + ) + with gr.Row(): + prompt_schedule_gen = gr.Textbox( + label="日程生成提示词", value=config_data["schedule"]["prompt_schedule_gen"], interactive=True + ) + with gr.Row(): + schedule_doing_update_interval = gr.Number(value=config_data["schedule"]["schedule_doing_update_interval"], + label="日程表更新间隔 单位秒", + interactive=True + ) + else: + with gr.Row(): + prompt_schedule_gen = gr.Textbox( + label="日程生成提示词", value=config_data["personality"]["prompt_schedule"], interactive=True + ) + enable_schedule_gen = gr.Checkbox(value=False,visible=False,interactive=False) + schedule_doing_update_interval = gr.Number(value=0,visible=False,interactive=False) with gr.Row(): personal_save_btn = gr.Button( "保存人格配置", @@ -1017,7 +1171,9 @@ with gr.Blocks(title="MaimBot配置文件编辑") as app: prompt_personality_1, prompt_personality_2, prompt_personality_3, - prompt_schedule, + enable_schedule_gen, + prompt_schedule_gen, + schedule_doing_update_interval, personality_1_probability, personality_2_probability, personality_3_probability, @@ -1027,11 +1183,14 @@ with gr.Blocks(title="MaimBot配置文件编辑") as app: with gr.TabItem("3-消息&表情包设置"): with gr.Row(): with gr.Column(scale=3): - with gr.Row(): - min_text_length = gr.Number( - value=config_data["message"]["min_text_length"], - label="与麦麦聊天时麦麦只会回答文本大于等于此数的消息", - ) + if PARSED_CONFIG_VERSION < version.parse("0.0.11"): + with gr.Row(): + min_text_length = gr.Number( + value=config_data["message"]["min_text_length"], + label="与麦麦聊天时麦麦只会回答文本大于等于此数的消息", + ) + else: + min_text_length = gr.Number(visible=False,value=0,interactive=False) with gr.Row(): max_context_size = gr.Number( value=config_data["message"]["max_context_size"], label="麦麦获得的上文数量" @@ -1049,21 +1208,27 @@ with gr.Blocks(title="MaimBot配置文件编辑") as app: value=config_data["message"]["thinking_timeout"], label="麦麦正在思考时,如果超过此秒数,则停止思考", ) - with gr.Row(): - response_willing_amplifier = gr.Number( - value=config_data["message"]["response_willing_amplifier"], - label="麦麦回复意愿放大系数,一般为1", - ) - with gr.Row(): - response_interested_rate_amplifier = gr.Number( - value=config_data["message"]["response_interested_rate_amplifier"], - label="麦麦回复兴趣度放大系数,听到记忆里的内容时放大系数", - ) - with gr.Row(): - down_frequency_rate = gr.Number( - value=config_data["message"]["down_frequency_rate"], - label="降低回复频率的群组回复意愿降低系数", - ) + if PARSED_CONFIG_VERSION < version.parse("0.0.11"): + with gr.Row(): + response_willing_amplifier = gr.Number( + value=config_data["message"]["response_willing_amplifier"], + label="麦麦回复意愿放大系数,一般为1", + ) + with gr.Row(): + response_interested_rate_amplifier = gr.Number( + value=config_data["message"]["response_interested_rate_amplifier"], + label="麦麦回复兴趣度放大系数,听到记忆里的内容时放大系数", + ) + with gr.Row(): + down_frequency_rate = gr.Number( + value=config_data["message"]["down_frequency_rate"], + label="降低回复频率的群组回复意愿降低系数", + ) + else: + response_willing_amplifier = gr.Number(visible=False,value=0,interactive=False) + response_interested_rate_amplifier = gr.Number(visible=False,value=0,interactive=False) + down_frequency_rate = gr.Number(visible=False,value=0,interactive=False) + with gr.Row(): gr.Markdown("### 违禁词列表") with gr.Row(): @@ -1207,7 +1372,7 @@ with gr.Blocks(title="MaimBot配置文件编辑") as app: ], outputs=[emoji_save_message], ) - with gr.TabItem("4-回复&模型设置"): + with gr.TabItem("4-意愿设置"): with gr.Row(): with gr.Column(scale=3): with gr.Row(): @@ -1229,6 +1394,55 @@ with gr.Blocks(title="MaimBot配置文件编辑") as app: ) else: willing_mode = gr.Textbox(visible=False, value="disabled") + if PARSED_CONFIG_VERSION >= version.parse("0.0.11"): + with gr.Row(): + response_willing_amplifier = gr.Number( + value=config_data["willing"]["response_willing_amplifier"], + label="麦麦回复意愿放大系数,一般为1", + ) + with gr.Row(): + response_interested_rate_amplifier = gr.Number( + value=config_data["willing"]["response_interested_rate_amplifier"], + label="麦麦回复兴趣度放大系数,听到记忆里的内容时放大系数", + ) + with gr.Row(): + down_frequency_rate = gr.Number( + value=config_data["willing"]["down_frequency_rate"], + label="降低回复频率的群组回复意愿降低系数", + ) + with gr.Row(): + emoji_response_penalty = gr.Number( + value=config_data["willing"]["emoji_response_penalty"], + label="表情包回复惩罚系数,设为0为不回复单个表情包,减少单独回复表情包的概率", + ) + else: + response_willing_amplifier = gr.Number(visible=False, value=1.0) + response_interested_rate_amplifier = gr.Number(visible=False, value=1.0) + down_frequency_rate = gr.Number(visible=False, value=1.0) + emoji_response_penalty = gr.Number(visible=False, value=1.0) + with gr.Row(): + willing_save_btn = gr.Button( + "保存意愿设置设置", + variant="primary", + elem_id="save_personality_btn", + elem_classes="save_personality_btn", + ) + with gr.Row(): + willing_save_message = gr.Textbox(label="意愿设置保存结果") + willing_save_btn.click( + save_willing_config, + inputs=[ + willing_mode, + response_willing_amplifier, + response_interested_rate_amplifier, + down_frequency_rate, + emoji_response_penalty, + ], + outputs=[emoji_save_message], + ) + with gr.TabItem("4-回复&模型设置"): + with gr.Row(): + with gr.Column(scale=3): with gr.Row(): model_r1_probability = gr.Slider( minimum=0, @@ -1289,10 +1503,13 @@ with gr.Blocks(title="MaimBot配置文件编辑") as app: inputs=[model_r1_probability, model_r2_probability, model_r3_probability], outputs=[model_warning_less_text], ) - with gr.Row(): - max_response_length = gr.Number( - value=config_data["response"]["max_response_length"], label="麦麦回答的最大token数" - ) + if PARSED_CONFIG_VERSION <= version.parse("0.0.10"): + with gr.Row(): + max_response_length = gr.Number( + value=config_data["response"]["max_response_length"], label="麦麦回答的最大token数" + ) + else: + max_response_length = gr.Number(visible=False,value=0) with gr.Row(): gr.Markdown("""### 模型设置""") with gr.Row(): @@ -1336,6 +1553,16 @@ with gr.Blocks(title="MaimBot配置文件编辑") as app: value=config_data["model"]["llm_normal"]["provider"], label="模型2提供商", ) + with gr.Row(): + model2_pri_in = gr.Number( + value=config_data["model"]["llm_normal"]["pri_in"], + label="模型2(次要回复模型)的输入价格(非必填,可以记录消耗)", + ) + with gr.Row(): + model2_pri_out = gr.Number( + value=config_data["model"]["llm_normal"]["pri_out"], + label="模型2(次要回复模型)的输出价格(非必填,可以记录消耗)", + ) with gr.TabItem("3-次要模型"): with gr.Row(): model3_name = gr.Textbox( @@ -1347,6 +1574,16 @@ with gr.Blocks(title="MaimBot配置文件编辑") as app: value=config_data["model"]["llm_reasoning_minor"]["provider"], label="模型3提供商", ) + with gr.Row(): + model3_pri_in = gr.Number( + value=config_data["model"]["llm_reasoning_minor"]["pri_in"], + label="模型3(次要回复模型)的输入价格(非必填,可以记录消耗)", + ) + with gr.Row(): + model3_pri_out = gr.Number( + value=config_data["model"]["llm_reasoning_minor"]["pri_out"], + label="模型3(次要回复模型)的输出价格(非必填,可以记录消耗)", + ) with gr.TabItem("4-情感&主题模型"): with gr.Row(): gr.Markdown("""### 情感模型设置""") @@ -1360,6 +1597,16 @@ with gr.Blocks(title="MaimBot配置文件编辑") as app: value=config_data["model"]["llm_emotion_judge"]["provider"], label="情感模型提供商", ) + with gr.Row(): + emotion_model_pri_in = gr.Number( + value=config_data["model"]["llm_emotion_judge"]["pri_in"], + label="情感模型的输入价格(非必填,可以记录消耗)", + ) + with gr.Row(): + emotion_model_pri_out = gr.Number( + value=config_data["model"]["llm_emotion_judge"]["pri_out"], + label="情感模型的输出价格(非必填,可以记录消耗)", + ) with gr.Row(): gr.Markdown("""### 主题模型设置""") with gr.Row(): @@ -1372,6 +1619,18 @@ with gr.Blocks(title="MaimBot配置文件编辑") as app: value=config_data["model"]["llm_topic_judge"]["provider"], label="主题判断模型提供商", ) + with gr.Row(): + topic_judge_model_pri_in = gr.Number( + value=config_data["model"]["llm_topic_judge"]["pri_in"], + label="主题判断模型的输入价格(非必填,可以记录消耗)", + ) + with gr.Row(): + topic_judge_model_pri_out = gr.Number( + value=config_data["model"]["llm_topic_judge"]["pri_out"], + label="主题判断模型的输出价格(非必填,可以记录消耗)", + ) + with gr.Row(): + gr.Markdown("""### 主题总结模型设置""") with gr.Row(): summary_by_topic_model_name = gr.Textbox( value=config_data["model"]["llm_summary_by_topic"]["name"], label="主题总结模型名称" @@ -1382,6 +1641,16 @@ with gr.Blocks(title="MaimBot配置文件编辑") as app: value=config_data["model"]["llm_summary_by_topic"]["provider"], label="主题总结模型提供商", ) + with gr.Row(): + summary_by_topic_model_pri_in = gr.Number( + value=config_data["model"]["llm_summary_by_topic"]["pri_in"], + label="主题总结模型的输入价格(非必填,可以记录消耗)", + ) + with gr.Row(): + summary_by_topic_model_pri_out = gr.Number( + value=config_data["model"]["llm_summary_by_topic"]["pri_out"], + label="主题总结模型的输出价格(非必填,可以记录消耗)", + ) with gr.TabItem("5-识图模型"): with gr.Row(): gr.Markdown("""### 识图模型设置""") @@ -1395,6 +1664,16 @@ with gr.Blocks(title="MaimBot配置文件编辑") as app: value=config_data["model"]["vlm"]["provider"], label="识图模型提供商", ) + with gr.Row(): + vlm_model_pri_in = gr.Number( + value=config_data["model"]["vlm"]["pri_in"], + label="识图模型的输入价格(非必填,可以记录消耗)", + ) + with gr.Row(): + vlm_model_pri_out = gr.Number( + value=config_data["model"]["vlm"]["pri_out"], + label="识图模型的输出价格(非必填,可以记录消耗)", + ) with gr.Row(): save_model_btn = gr.Button("保存回复&模型设置", variant="primary", elem_id="save_model_btn") with gr.Row(): @@ -1413,16 +1692,28 @@ with gr.Blocks(title="MaimBot配置文件编辑") as app: model1_pri_out, model2_name, model2_provider, + model2_pri_in, + model2_pri_out, model3_name, model3_provider, + model3_pri_in, + model3_pri_out, emotion_model_name, emotion_model_provider, + emotion_model_pri_in, + emotion_model_pri_out, topic_judge_model_name, topic_judge_model_provider, + topic_judge_model_pri_in, + topic_judge_model_pri_out, summary_by_topic_model_name, summary_by_topic_model_provider, + summary_by_topic_model_pri_in, + summary_by_topic_model_pri_out, vlm_model_name, vlm_model_provider, + vlm_model_pri_in, + vlm_model_pri_out, ], outputs=[save_btn_message], ) @@ -1436,6 +1727,61 @@ with gr.Blocks(title="MaimBot配置文件编辑") as app: value=config_data["memory"]["build_memory_interval"], label="记忆构建间隔 单位秒,间隔越低,麦麦学习越多,但是冗余信息也会增多", ) + if PARSED_CONFIG_VERSION >= version.parse("0.0.11"): + with gr.Row(): + gr.Markdown("---") + with gr.Row(): + gr.Markdown("""### 记忆构建分布设置""") + with gr.Row(): + gr.Markdown("""记忆构建分布参数说明:\n + 分布1均值:第一个正态分布的均值\n + 分布1标准差:第一个正态分布的标准差\n + 分布1权重:第一个正态分布的权重\n + 分布2均值:第二个正态分布的均值\n + 分布2标准差:第二个正态分布的标准差\n + 分布2权重:第二个正态分布的权重 + """) + with gr.Row(): + with gr.Column(scale=1): + build_memory_dist1_mean = gr.Number( + value=config_data["memory"].get("build_memory_distribution", [4.0,2.0,0.6,24.0,8.0,0.4])[0], + label="分布1均值", + ) + with gr.Column(scale=1): + build_memory_dist1_std = gr.Number( + value=config_data["memory"].get("build_memory_distribution", [4.0,2.0,0.6,24.0,8.0,0.4])[1], + label="分布1标准差", + ) + with gr.Column(scale=1): + build_memory_dist1_weight = gr.Number( + value=config_data["memory"].get("build_memory_distribution", [4.0,2.0,0.6,24.0,8.0,0.4])[2], + label="分布1权重", + ) + with gr.Row(): + with gr.Column(scale=1): + build_memory_dist2_mean = gr.Number( + value=config_data["memory"].get("build_memory_distribution", [4.0,2.0,0.6,24.0,8.0,0.4])[3], + label="分布2均值", + ) + with gr.Column(scale=1): + build_memory_dist2_std = gr.Number( + value=config_data["memory"].get("build_memory_distribution", [4.0,2.0,0.6,24.0,8.0,0.4])[4], + label="分布2标准差", + ) + with gr.Column(scale=1): + build_memory_dist2_weight = gr.Number( + value=config_data["memory"].get("build_memory_distribution", [4.0,2.0,0.6,24.0,8.0,0.4])[5], + label="分布2权重", + ) + with gr.Row(): + gr.Markdown("---") + else: + build_memory_dist1_mean = gr.Number(value=0.0,visible=False,interactive=False) + build_memory_dist1_std = gr.Number(value=0.0,visible=False,interactive=False) + build_memory_dist1_weight = gr.Number(value=0.0,visible=False,interactive=False) + build_memory_dist2_mean = gr.Number(value=0.0,visible=False,interactive=False) + build_memory_dist2_std = gr.Number(value=0.0,visible=False,interactive=False) + build_memory_dist2_weight = gr.Number(value=0.0,visible=False,interactive=False) with gr.Row(): memory_compress_rate = gr.Number( value=config_data["memory"]["memory_compress_rate"], @@ -1538,6 +1884,12 @@ with gr.Blocks(title="MaimBot配置文件编辑") as app: mood_update_interval, mood_decay_rate, mood_intensity_factor, + build_memory_dist1_mean, + build_memory_dist1_std, + build_memory_dist1_weight, + build_memory_dist2_mean, + build_memory_dist2_std, + build_memory_dist2_weight, ], outputs=[save_memory_mood_message], ) @@ -1709,22 +2061,31 @@ with gr.Blocks(title="MaimBot配置文件编辑") as app: keywords_reaction_enabled = gr.Checkbox( value=config_data["keywords_reaction"]["enable"], label="是否针对某个关键词作出反应" ) - with gr.Row(): - enable_advance_output = gr.Checkbox( - value=config_data["others"]["enable_advance_output"], label="是否开启高级输出" - ) - with gr.Row(): - enable_kuuki_read = gr.Checkbox( - value=config_data["others"]["enable_kuuki_read"], label="是否启用读空气功能" - ) - with gr.Row(): - enable_debug_output = gr.Checkbox( - value=config_data["others"]["enable_debug_output"], label="是否开启调试输出" - ) - with gr.Row(): - enable_friend_chat = gr.Checkbox( - value=config_data["others"]["enable_friend_chat"], label="是否开启好友聊天" - ) + if PARSED_CONFIG_VERSION <= version.parse("0.0.10"): + with gr.Row(): + enable_advance_output = gr.Checkbox( + value=config_data["others"]["enable_advance_output"], label="是否开启高级输出" + ) + with gr.Row(): + enable_kuuki_read = gr.Checkbox( + value=config_data["others"]["enable_kuuki_read"], label="是否启用读空气功能" + ) + with gr.Row(): + enable_debug_output = gr.Checkbox( + value=config_data["others"]["enable_debug_output"], label="是否开启调试输出" + ) + with gr.Row(): + enable_friend_chat = gr.Checkbox( + value=config_data["others"]["enable_friend_chat"], label="是否开启好友聊天" + ) + elif PARSED_CONFIG_VERSION >= version.parse("0.0.11"): + with gr.Row(): + enable_friend_chat = gr.Checkbox( + value=config_data["experimental"]["enable_friend_chat"], label="是否开启好友聊天" + ) + enable_advance_output = gr.Checkbox(value=False,visible=False,interactive=False) + enable_kuuki_read = gr.Checkbox(value=False,visible=False,interactive=False) + enable_debug_output = gr.Checkbox(value=False,visible=False,interactive=False) if PARSED_CONFIG_VERSION > HAVE_ONLINE_STATUS_VERSION: with gr.Row(): gr.Markdown( @@ -1736,7 +2097,28 @@ with gr.Blocks(title="MaimBot配置文件编辑") as app: remote_status = gr.Checkbox( value=config_data["remote"]["enable"], label="是否开启麦麦在线全球统计" ) - + if PARSED_CONFIG_VERSION >= version.parse("0.0.11"): + with gr.Row(): + gr.Markdown("""### 回复分割器设置""") + with gr.Row(): + enable_response_spliter = gr.Checkbox( + value=config_data["response_spliter"]["enable_response_spliter"], + label="是否启用回复分割器" + ) + with gr.Row(): + response_max_length = gr.Number( + value=config_data["response_spliter"]["response_max_length"], + label="回复允许的最大长度" + ) + with gr.Row(): + response_max_sentence_num = gr.Number( + value=config_data["response_spliter"]["response_max_sentence_num"], + label="回复允许的最大句子数" + ) + else: + enable_response_spliter = gr.Checkbox(value=False,visible=False,interactive=False) + response_max_length = gr.Number(value=0,visible=False,interactive=False) + response_max_sentence_num = gr.Number(value=0,visible=False,interactive=False) with gr.Row(): gr.Markdown("""### 中文错别字设置""") with gr.Row(): @@ -1790,14 +2172,56 @@ with gr.Blocks(title="MaimBot配置文件编辑") as app: tone_error_rate, word_replace_rate, remote_status, + enable_response_spliter, + response_max_length, + response_max_sentence_num ], outputs=[save_other_config_message], ) - app.queue().launch( # concurrency_count=511, max_size=1022 - server_name="0.0.0.0", - inbrowser=True, - share=is_share, - server_port=7000, - debug=debug, - quiet=True, - ) +# 检查端口是否可用 +def is_port_available(port, host='0.0.0.0'): + """检查指定的端口是否可用""" + try: + # 创建一个socket对象 + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + # 设置socket重用地址选项 + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + # 尝试绑定端口 + sock.bind((host, port)) + # 如果成功绑定,则关闭socket并返回True + sock.close() + return True + except socket.error: + # 如果绑定失败,说明端口已被占用 + return False + + + # 寻找可用端口 +def find_available_port(start_port=7000, max_port=8000): + """ + 从start_port开始,寻找可用的端口 + 如果端口被占用,尝试下一个端口,直到找到可用端口或达到max_port + """ + port = start_port + while port <= max_port: + if is_port_available(port): + logger.info(f"找到可用端口: {port}") + return port + logger.warning(f"端口 {port} 已被占用,尝试下一个端口") + port += 1 + # 如果所有端口都被占用,返回None + logger.error(f"无法找到可用端口 (已尝试 {start_port}-{max_port})") + return None + +# 寻找可用端口 +launch_port = find_available_port(7000, 8000) or 7000 + +app.queue().launch( # concurrency_count=511, max_size=1022 + server_name="0.0.0.0", + inbrowser=True, + share=is_share, + server_port=launch_port, + debug=debug, + quiet=True, +) + From 62ec0b411b2b627ca243b719c480118f972fd072 Mon Sep 17 00:00:00 2001 From: DrSmoothl <1787882683@qq.com> Date: Thu, 27 Mar 2025 19:01:58 +0800 Subject: [PATCH 37/46] =?UTF-8?q?=E8=BF=87webui=E7=9A=84Ruff=E6=A3=80?= =?UTF-8?q?=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- webui.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/webui.py b/webui.py index d45259dcc..e4617e2f6 100644 --- a/webui.py +++ b/webui.py @@ -1744,33 +1744,51 @@ with gr.Blocks(title="MaimBot配置文件编辑") as app: with gr.Row(): with gr.Column(scale=1): build_memory_dist1_mean = gr.Number( - value=config_data["memory"].get("build_memory_distribution", [4.0,2.0,0.6,24.0,8.0,0.4])[0], + value=config_data["memory"].get( + "build_memory_distribution", + [4.0,2.0,0.6,24.0,8.0,0.4] + )[0], label="分布1均值", ) with gr.Column(scale=1): build_memory_dist1_std = gr.Number( - value=config_data["memory"].get("build_memory_distribution", [4.0,2.0,0.6,24.0,8.0,0.4])[1], + value=config_data["memory"].get( + "build_memory_distribution", + [4.0,2.0,0.6,24.0,8.0,0.4] + )[1], label="分布1标准差", ) with gr.Column(scale=1): build_memory_dist1_weight = gr.Number( - value=config_data["memory"].get("build_memory_distribution", [4.0,2.0,0.6,24.0,8.0,0.4])[2], + value=config_data["memory"].get( + "build_memory_distribution", + [4.0,2.0,0.6,24.0,8.0,0.4] + )[2], label="分布1权重", ) with gr.Row(): with gr.Column(scale=1): build_memory_dist2_mean = gr.Number( - value=config_data["memory"].get("build_memory_distribution", [4.0,2.0,0.6,24.0,8.0,0.4])[3], + value=config_data["memory"].get( + "build_memory_distribution", + [4.0,2.0,0.6,24.0,8.0,0.4] + )[3], label="分布2均值", ) with gr.Column(scale=1): build_memory_dist2_std = gr.Number( - value=config_data["memory"].get("build_memory_distribution", [4.0,2.0,0.6,24.0,8.0,0.4])[4], + value=config_data["memory"].get( + "build_memory_distribution", + [4.0,2.0,0.6,24.0,8.0,0.4] + )[4], label="分布2标准差", ) with gr.Column(scale=1): build_memory_dist2_weight = gr.Number( - value=config_data["memory"].get("build_memory_distribution", [4.0,2.0,0.6,24.0,8.0,0.4])[5], + value=config_data["memory"].get( + "build_memory_distribution", + [4.0,2.0,0.6,24.0,8.0,0.4] + )[5], label="分布2权重", ) with gr.Row(): From 2812b0df3c1d652f48c47563e989403f09208dc5 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 27 Mar 2025 20:07:01 +0800 Subject: [PATCH 38/46] =?UTF-8?q?fix:=E8=A7=A3=E8=80=A6=E6=B5=B7=E9=A9=AC?= =?UTF-8?q?=E4=BD=93=EF=BC=8C=E8=8E=B2=E8=97=95=E4=BF=83=E9=94=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/__init__.py | 25 ++++- src/plugins/chat/bot.py | 5 +- src/plugins/chat/prompt_builder.py | 17 ++-- src/plugins/memory_system/Hippocampus.py | 94 +++++++++++++++---- src/plugins/memory_system/__init__.py | 0 src/plugins/memory_system/config.py | 4 +- .../{memory.py => memory_deprecated.py} | 4 +- src/plugins/schedule/schedule_generator.py | 4 +- 8 files changed, 116 insertions(+), 37 deletions(-) delete mode 100644 src/plugins/memory_system/__init__.py rename src/plugins/memory_system/{memory.py => memory_deprecated.py} (99%) diff --git a/src/plugins/chat/__init__.py b/src/plugins/chat/__init__.py index 55b83e889..7c3629f41 100644 --- a/src/plugins/chat/__init__.py +++ b/src/plugins/chat/__init__.py @@ -14,7 +14,8 @@ from .emoji_manager import emoji_manager from .relationship_manager import relationship_manager from ..willing.willing_manager import willing_manager from .chat_stream import chat_manager -from ..memory_system.memory import hippocampus +# from ..memory_system.memory import hippocampus +from src.plugins.memory_system.Hippocampus import HippocampusManager from .message_sender import message_manager, message_sender from .storage import MessageStorage from src.common.logger import get_module_logger @@ -59,6 +60,22 @@ async def start_think_flow(): logger.error(f"启动大脑和外部世界失败: {e}") raise +async def start_memory(): + """启动记忆系统""" + try: + start_time = time.time() + logger.info("开始初始化记忆系统...") + + # 使用HippocampusManager初始化海马体 + hippocampus_manager = HippocampusManager.get_instance() + hippocampus_manager.initialize(global_config=global_config) + + end_time = time.time() + logger.success(f"记忆系统初始化完成,耗时: {end_time - start_time:.2f} 秒") + except Exception as e: + logger.error(f"记忆系统初始化失败: {e}") + raise + @driver.on_startup async def start_background_tasks(): @@ -79,6 +96,8 @@ async def start_background_tasks(): # 只启动表情包管理任务 asyncio.create_task(emoji_manager.start_periodic_check()) + + asyncio.create_task(start_memory()) @driver.on_startup @@ -139,14 +158,14 @@ async def _(bot: Bot, event: NoticeEvent, state: T_State): @scheduler.scheduled_job("interval", seconds=global_config.build_memory_interval, id="build_memory") async def build_memory_task(): """每build_memory_interval秒执行一次记忆构建""" - await hippocampus.operation_build_memory() + await HippocampusManager.get_instance().build_memory() @scheduler.scheduled_job("interval", seconds=global_config.forget_memory_interval, id="forget_memory") async def forget_memory_task(): """每30秒执行一次记忆构建""" print("\033[1;32m[记忆遗忘]\033[0m 开始遗忘记忆...") - await hippocampus.operation_forget_topic(percentage=global_config.memory_forget_percentage) + await HippocampusManager.get_instance().forget_memory(percentage=global_config.memory_forget_percentage) print("\033[1;32m[记忆遗忘]\033[0m 记忆遗忘完成") diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index e89375217..21557ef60 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -12,7 +12,7 @@ from nonebot.adapters.onebot.v11 import ( FriendRecallNoticeEvent, ) -from ..memory_system.memory import hippocampus +from ..memory_system.Hippocampus import HippocampusManager from ..moods.moods import MoodManager # 导入情绪管理器 from .config import global_config from .emoji_manager import emoji_manager # 导入表情包管理器 @@ -129,7 +129,8 @@ class ChatBot: # 根据话题计算激活度 topic = "" - interested_rate = await hippocampus.memory_activate_value(message.processed_plain_text) / 100 + # interested_rate = await HippocampusManager.get_instance().memory_activate_value(message.processed_plain_text) / 100 + interested_rate = 0.1 logger.debug(f"对{message.processed_plain_text}的激活度:{interested_rate}") # logger.info(f"\033[1;32m[主题识别]\033[0m 使用{global_config.topic_extract}主题: {topic}") diff --git a/src/plugins/chat/prompt_builder.py b/src/plugins/chat/prompt_builder.py index dc2e5930e..672471d74 100644 --- a/src/plugins/chat/prompt_builder.py +++ b/src/plugins/chat/prompt_builder.py @@ -3,7 +3,7 @@ import time from typing import Optional from ...common.database import db -from ..memory_system.memory import hippocampus, memory_graph +from ..memory_system.Hippocampus import HippocampusManager from ..moods.moods import MoodManager from ..schedule.schedule_generator import bot_schedule from .config import global_config @@ -79,19 +79,20 @@ class PromptBuilder: start_time = time.time() # 调用 hippocampus 的 get_relevant_memories 方法 - relevant_memories = await hippocampus.get_relevant_memories( - text=message_txt, max_topics=3, similarity_threshold=0.5, max_memory_num=4 + relevant_memories = await HippocampusManager.get_instance().get_memory_from_text( + text=message_txt, num=3, max_depth=2, fast_retrieval=True ) + memory_str = "\n".join(memory for topic, memories, _ in relevant_memories for memory in memories) + print(f"memory_str: {memory_str}") if relevant_memories: # 格式化记忆内容 - memory_str = "\n".join(m["content"] for m in relevant_memories) memory_prompt = f"你回忆起:\n{memory_str}\n" # 打印调试信息 logger.debug("[记忆检索]找到以下相关记忆:") - for memory in relevant_memories: - logger.debug(f"- 主题「{memory['topic']}」[相似度: {memory['similarity']:.2f}]: {memory['content']}") + # for topic, memory_items, similarity in relevant_memories: + # logger.debug(f"- 主题「{topic}」[相似度: {similarity:.2f}]: {memory_items}") end_time = time.time() logger.info(f"回忆耗时: {(end_time - start_time):.3f}秒") @@ -192,7 +193,7 @@ class PromptBuilder: # print(f"\033[1;34m[调试]\033[0m 已从数据库获取群 {group_id} 的消息记录:{chat_talking_prompt}") # 获取主动发言的话题 - all_nodes = memory_graph.dots + all_nodes = HippocampusManager.get_instance().memory_graph.dots all_nodes = filter(lambda dot: len(dot[1]["memory_items"]) > 3, all_nodes) nodes_for_select = random.sample(all_nodes, 5) topics = [info[0] for info in nodes_for_select] @@ -245,7 +246,7 @@ class PromptBuilder: related_info = "" logger.debug(f"获取知识库内容,元消息:{message[:30]}...,消息长度: {len(message)}") embedding = await get_embedding(message, request_type="prompt_build") - related_info += self.get_info_from_db(embedding, threshold=threshold) + related_info += self.get_info_from_db(embedding, limit=1, threshold=threshold) return related_info diff --git a/src/plugins/memory_system/Hippocampus.py b/src/plugins/memory_system/Hippocampus.py index 67363e95e..d8f976fc4 100644 --- a/src/plugins/memory_system/Hippocampus.py +++ b/src/plugins/memory_system/Hippocampus.py @@ -4,7 +4,6 @@ import math import random import time import re - import jieba import networkx as nx @@ -15,7 +14,6 @@ from ..chat.utils import ( calculate_information_content, cosine_similarity, get_closest_chat_from_db, - text_to_vector, ) from ..models.utils_model import LLM_request from src.common.logger import get_module_logger, LogConfig, MEMORY_STYLE_CONFIG @@ -180,7 +178,7 @@ class EntorhinalCortex: max_memorized_time_per_msg = 3 # 创建双峰分布的记忆调度器 - scheduler = MemoryBuildScheduler( + sample_scheduler = MemoryBuildScheduler( n_hours1=self.config.memory_build_distribution[0], std_hours1=self.config.memory_build_distribution[1], weight1=self.config.memory_build_distribution[2], @@ -190,7 +188,7 @@ class EntorhinalCortex: total_samples=self.config.build_memory_sample_num ) - timestamps = scheduler.get_timestamp_array() + timestamps = sample_scheduler.get_timestamp_array() logger.info(f"回忆往事: {[time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(ts)) for ts in timestamps]}") chat_samples = [] for timestamp in timestamps: @@ -674,8 +672,8 @@ class Hippocampus: self.parahippocampal_gyrus = ParahippocampalGyrus(self) # 从数据库加载记忆图 self.entorhinal_cortex.sync_memory_from_db() - self.llm_topic_judge = self.config.llm_topic_judge - self.llm_summary_by_topic = self.config.llm_summary_by_topic + self.llm_topic_judge = LLM_request(self.config.llm_topic_judge) + self.llm_summary_by_topic = LLM_request(self.config.llm_summary_by_topic) def get_all_node_names(self) -> list: """获取记忆图中所有节点的名字列表""" @@ -831,19 +829,79 @@ class Hippocampus: unique_memories.sort(key=lambda x: x[2], reverse=True) return unique_memories[:num] -# driver = get_driver() -# config = driver.config +class HippocampusManager: + _instance = None + _hippocampus = None + _global_config = None + _initialized = False -start_time = time.time() + @classmethod + def get_instance(cls): + if cls._instance is None: + cls._instance = cls() + return cls._instance -# 创建记忆图 -memory_graph = Memory_graph() -# 创建海马体 -hippocampus = Hippocampus() + @classmethod + def get_hippocampus(cls): + if not cls._initialized: + raise RuntimeError("HippocampusManager 尚未初始化,请先调用 initialize 方法") + return cls._hippocampus + + def initialize(self, global_config): + """初始化海马体实例""" + if self._initialized: + return self._hippocampus + + self._global_config = global_config + self._hippocampus = Hippocampus() + self._hippocampus.initialize(global_config) + self._initialized = True + + # 输出记忆系统参数信息 + config = self._hippocampus.config + logger.success("--------------------------------") + logger.success("记忆系统参数配置:") + logger.success(f"记忆构建间隔: {global_config.build_memory_interval}秒") + logger.success(f"记忆遗忘间隔: {global_config.forget_memory_interval}秒") + logger.success(f"记忆遗忘比例: {global_config.memory_forget_percentage}") + logger.success(f"记忆压缩率: {config.memory_compress_rate}") + logger.success(f"记忆构建样本数: {config.build_memory_sample_num}") + logger.success(f"记忆构建样本长度: {config.build_memory_sample_length}") + logger.success(f"记忆遗忘时间: {config.memory_forget_time}小时") + logger.success(f"记忆构建分布: {config.memory_build_distribution}") + logger.success("--------------------------------") + + return self._hippocampus + + async def build_memory(self): + """构建记忆的公共接口""" + if not self._initialized: + raise RuntimeError("HippocampusManager 尚未初始化,请先调用 initialize 方法") + return await self._hippocampus.parahippocampal_gyrus.operation_build_memory() + + async def forget_memory(self, percentage: float = 0.1): + """遗忘记忆的公共接口""" + if not self._initialized: + raise RuntimeError("HippocampusManager 尚未初始化,请先调用 initialize 方法") + return await self._hippocampus.parahippocampal_gyrus.operation_forget_topic(percentage) + + async def get_memory_from_text(self, text: str, num: int = 5, max_depth: int = 2, + fast_retrieval: bool = False) -> list: + """从文本中获取相关记忆的公共接口""" + if not self._initialized: + raise RuntimeError("HippocampusManager 尚未初始化,请先调用 initialize 方法") + return await self._hippocampus.get_memory_from_text(text, num, max_depth, fast_retrieval) + + def get_memory_from_keyword(self, keyword: str, max_depth: int = 2) -> list: + """从关键词获取相关记忆的公共接口""" + if not self._initialized: + raise RuntimeError("HippocampusManager 尚未初始化,请先调用 initialize 方法") + return self._hippocampus.get_memory_from_keyword(keyword, max_depth) + + def get_all_node_names(self) -> list: + """获取所有节点名称的公共接口""" + if not self._initialized: + raise RuntimeError("HippocampusManager 尚未初始化,请先调用 initialize 方法") + return self._hippocampus.get_all_node_names() -# 从全局配置初始化记忆系统 -from ..chat.config import global_config -hippocampus.initialize(global_config=global_config) -end_time = time.time() -logger.success(f"加载海马体耗时: {end_time - start_time:.2f} 秒") diff --git a/src/plugins/memory_system/__init__.py b/src/plugins/memory_system/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/plugins/memory_system/config.py b/src/plugins/memory_system/config.py index fe688372f..6c49d15fc 100644 --- a/src/plugins/memory_system/config.py +++ b/src/plugins/memory_system/config.py @@ -29,6 +29,6 @@ class MemoryConfig: memory_compress_rate=global_config.memory_compress_rate, memory_forget_time=global_config.memory_forget_time, memory_ban_words=global_config.memory_ban_words, - llm_topic_judge=global_config.topic_judge_model, - llm_summary_by_topic=global_config.summary_by_topic_model + llm_topic_judge=global_config.llm_topic_judge, + llm_summary_by_topic=global_config.llm_summary_by_topic ) \ No newline at end of file diff --git a/src/plugins/memory_system/memory.py b/src/plugins/memory_system/memory_deprecated.py similarity index 99% rename from src/plugins/memory_system/memory.py rename to src/plugins/memory_system/memory_deprecated.py index e0151c04c..3760b2ac9 100644 --- a/src/plugins/memory_system/memory.py +++ b/src/plugins/memory_system/memory_deprecated.py @@ -227,7 +227,7 @@ class Hippocampus: max_memorized_time_per_msg = 3 # 创建双峰分布的记忆调度器 - scheduler = MemoryBuildScheduler( + sample_scheduler = MemoryBuildScheduler( n_hours1=global_config.memory_build_distribution[0], # 第一个分布均值(4小时前) std_hours1=global_config.memory_build_distribution[1], # 第一个分布标准差 weight1=global_config.memory_build_distribution[2], # 第一个分布权重 60% @@ -238,7 +238,7 @@ class Hippocampus: ) # 生成时间戳数组 - timestamps = scheduler.get_timestamp_array() + timestamps = sample_scheduler.get_timestamp_array() # logger.debug(f"生成的时间戳数组: {timestamps}") # print(f"生成的时间戳数组: {timestamps}") # print(f"时间戳的实际时间: {[time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(ts)) for ts in timestamps]}") diff --git a/src/plugins/schedule/schedule_generator.py b/src/plugins/schedule/schedule_generator.py index f4bbb42b0..fb79216d5 100644 --- a/src/plugins/schedule/schedule_generator.py +++ b/src/plugins/schedule/schedule_generator.py @@ -140,8 +140,8 @@ class ScheduleGenerator: if mind_thinking: prompt += f"你脑子里在想:{mind_thinking}\n" prompt += f"现在是{now_time},结合你的个人特点和行为习惯,注意关注你今天的日程安排和想法,这很重要," - prompt += "推测你现在和之后做什么,具体一些,详细一些\n" - prompt += "直接返回你在做的事情,不要输出其他内容:" + prompt += "推测你现在在做什么,具体一些,详细一些\n" + prompt += "直接返回你在做的事情,注意是当前时间,不要输出其他内容:" return prompt async def generate_daily_schedule( From ebf8218377ca9f9ad2e8103990ddd348705d113f Mon Sep 17 00:00:00 2001 From: DrSmoothl <1787882683@qq.com> Date: Thu, 27 Mar 2025 22:13:35 +0800 Subject: [PATCH 39/46] =?UTF-8?q?=E8=BF=87ruff=E5=96=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- webui.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/webui.py b/webui.py index e4617e2f6..9c1a0ad6d 100644 --- a/webui.py +++ b/webui.py @@ -1145,10 +1145,11 @@ with gr.Blocks(title="MaimBot配置文件编辑") as app: label="日程生成提示词", value=config_data["schedule"]["prompt_schedule_gen"], interactive=True ) with gr.Row(): - schedule_doing_update_interval = gr.Number(value=config_data["schedule"]["schedule_doing_update_interval"], - label="日程表更新间隔 单位秒", - interactive=True - ) + schedule_doing_update_interval = gr.Number( + value=config_data["schedule"]["schedule_doing_update_interval"], + label="日程表更新间隔 单位秒", + interactive=True + ) else: with gr.Row(): prompt_schedule_gen = gr.Textbox( From b474da38754d4e8598c2ac00a4b3806eb766c11f Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 27 Mar 2025 22:14:23 +0800 Subject: [PATCH 40/46] =?UTF-8?q?better:=E6=B5=B7=E9=A9=AC=E4=BD=932.0?= =?UTF-8?q?=E5=8D=87=E7=BA=A7-=E8=BF=9B=E5=BA=A630%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/__init__.py | 2 +- src/plugins/chat/bot.py | 2 +- src/plugins/chat/cq_code.py | 2 +- src/plugins/chat/emoji_manager.py | 2 +- src/plugins/chat/llm_generator.py | 2 +- src/plugins/chat/message_sender.py | 2 +- src/plugins/chat/prompt_builder.py | 5 +- src/plugins/chat/topic_identifier.py | 2 +- src/plugins/chat/utils.py | 56 +- src/plugins/chat/utils_image.py | 2 +- src/plugins/chat/utils_user.py | 2 +- src/plugins/{chat => config}/config.py | 0 src/plugins/config/config_env.py | 55 + src/plugins/memory_system/Hippocampus.py | 288 ++++- src/plugins/memory_system/debug_memory.py | 94 ++ src/plugins/memory_system/draw_memory.py | 298 ----- .../{config.py => memory_config.py} | 0 .../memory_system/memory_deprecated.py | 1006 ----------------- .../memory_system/memory_manual_build.py | 992 ---------------- src/plugins/memory_system/offline_llm.py | 2 +- src/plugins/models/utils_model.py | 11 +- src/plugins/moods/moods.py | 2 +- src/plugins/remote/remote.py | 2 +- src/plugins/schedule/schedule_generator.py | 2 +- src/plugins/willing/mode_classical.py | 2 +- src/plugins/willing/mode_dynamic.py | 2 +- src/plugins/willing/willing_manager.py | 2 +- src/think_flow_demo/current_mind.py | 2 +- src/think_flow_demo/heartflow.py | 2 +- src/think_flow_demo/outer_world.py | 2 +- 30 files changed, 433 insertions(+), 2410 deletions(-) rename src/plugins/{chat => config}/config.py (100%) create mode 100644 src/plugins/config/config_env.py create mode 100644 src/plugins/memory_system/debug_memory.py delete mode 100644 src/plugins/memory_system/draw_memory.py rename src/plugins/memory_system/{config.py => memory_config.py} (100%) delete mode 100644 src/plugins/memory_system/memory_deprecated.py delete mode 100644 src/plugins/memory_system/memory_manual_build.py diff --git a/src/plugins/chat/__init__.py b/src/plugins/chat/__init__.py index 7c3629f41..e598115ac 100644 --- a/src/plugins/chat/__init__.py +++ b/src/plugins/chat/__init__.py @@ -9,7 +9,7 @@ from ..moods.moods import MoodManager # 导入情绪管理器 from ..schedule.schedule_generator import bot_schedule from ..utils.statistic import LLMStatistics from .bot import chat_bot -from .config import global_config +from ..config.config import global_config from .emoji_manager import emoji_manager from .relationship_manager import relationship_manager from ..willing.willing_manager import willing_manager diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 21557ef60..cc3c43526 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -14,7 +14,7 @@ from nonebot.adapters.onebot.v11 import ( from ..memory_system.Hippocampus import HippocampusManager from ..moods.moods import MoodManager # 导入情绪管理器 -from .config import global_config +from ..config.config import global_config from .emoji_manager import emoji_manager # 导入表情包管理器 from .llm_generator import ResponseGenerator from .message import MessageSending, MessageRecv, MessageThinking, MessageSet diff --git a/src/plugins/chat/cq_code.py b/src/plugins/chat/cq_code.py index 46b4c891f..e456bad90 100644 --- a/src/plugins/chat/cq_code.py +++ b/src/plugins/chat/cq_code.py @@ -10,7 +10,7 @@ from src.common.logger import get_module_logger from nonebot import get_driver from ..models.utils_model import LLM_request -from .config import global_config +from ..config.config import global_config from .mapper import emojimapper from .message_base import Seg from .utils_user import get_user_nickname, get_groupname diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py index 20a5c3b1b..ccfb290cb 100644 --- a/src/plugins/chat/emoji_manager.py +++ b/src/plugins/chat/emoji_manager.py @@ -12,7 +12,7 @@ import io from nonebot import get_driver from ...common.database import db -from ..chat.config import global_config +from ..config.config import global_config from ..chat.utils import get_embedding from ..chat.utils_image import ImageManager, image_path_to_base64 from ..models.utils_model import LLM_request diff --git a/src/plugins/chat/llm_generator.py b/src/plugins/chat/llm_generator.py index 7b032104a..f2a94acf6 100644 --- a/src/plugins/chat/llm_generator.py +++ b/src/plugins/chat/llm_generator.py @@ -6,7 +6,7 @@ from nonebot import get_driver from ...common.database import db from ..models.utils_model import LLM_request -from .config import global_config +from ..config.config import global_config from .message import MessageRecv, MessageThinking, Message from .prompt_builder import prompt_builder from .utils import process_llm_response diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index 7528a2e5a..d57bd3f48 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -9,7 +9,7 @@ from .message_cq import MessageSendCQ from .message import MessageSending, MessageThinking, MessageSet from .storage import MessageStorage -from .config import global_config +from ..config.config import global_config from .utils import truncate_message, calculate_typing_time from src.common.logger import LogConfig, SENDER_STYLE_CONFIG diff --git a/src/plugins/chat/prompt_builder.py b/src/plugins/chat/prompt_builder.py index 672471d74..ea4550329 100644 --- a/src/plugins/chat/prompt_builder.py +++ b/src/plugins/chat/prompt_builder.py @@ -6,7 +6,7 @@ from ...common.database import db from ..memory_system.Hippocampus import HippocampusManager from ..moods.moods import MoodManager from ..schedule.schedule_generator import bot_schedule -from .config import global_config +from ..config.config import global_config from .utils import get_embedding, get_recent_group_detailed_plain_text, get_recent_group_speaker from .chat_stream import chat_manager from .relationship_manager import relationship_manager @@ -82,7 +82,8 @@ class PromptBuilder: relevant_memories = await HippocampusManager.get_instance().get_memory_from_text( text=message_txt, num=3, max_depth=2, fast_retrieval=True ) - memory_str = "\n".join(memory for topic, memories, _ in relevant_memories for memory in memories) + # memory_str = "\n".join(memory for topic, memories, _ in relevant_memories for memory in memories) + memory_str = "" print(f"memory_str: {memory_str}") if relevant_memories: diff --git a/src/plugins/chat/topic_identifier.py b/src/plugins/chat/topic_identifier.py index 6e11bc9d7..15df925db 100644 --- a/src/plugins/chat/topic_identifier.py +++ b/src/plugins/chat/topic_identifier.py @@ -3,7 +3,7 @@ from typing import List, Optional from nonebot import get_driver from ..models.utils_model import LLM_request -from .config import global_config +from ..config.config import global_config from src.common.logger import get_module_logger, LogConfig, TOPIC_STYLE_CONFIG # 定义日志配置 diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index ef9878c4e..1b57212a9 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -12,7 +12,7 @@ from src.common.logger import get_module_logger from ..models.utils_model import LLM_request from ..utils.typo_generator import ChineseTypoGenerator -from .config import global_config +from ..config.config import global_config from .message import MessageRecv, Message from .message_base import UserInfo from .chat_stream import ChatStream @@ -62,60 +62,6 @@ async def get_embedding(text, request_type="embedding"): return await llm.get_embedding(text) -def calculate_information_content(text): - """计算文本的信息量(熵)""" - char_count = Counter(text) - total_chars = len(text) - - entropy = 0 - for count in char_count.values(): - probability = count / total_chars - entropy -= probability * math.log2(probability) - - return entropy - - -def get_closest_chat_from_db(length: int, timestamp: str): - # print(f"获取最接近指定时间戳的聊天记录,长度: {length}, 时间戳: {timestamp}") - # print(f"当前时间: {timestamp},转换后时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(timestamp))}") - chat_records = [] - closest_record = db.messages.find_one({"time": {"$lte": timestamp}}, sort=[("time", -1)]) - # print(f"最接近的记录: {closest_record}") - if closest_record: - closest_time = closest_record["time"] - chat_id = closest_record["chat_id"] # 获取chat_id - # 获取该时间戳之后的length条消息,保持相同的chat_id - chat_records = list( - db.messages.find( - { - "time": {"$gt": closest_time}, - "chat_id": chat_id, # 添加chat_id过滤 - } - ) - .sort("time", 1) - .limit(length) - ) - # print(f"获取到的记录: {chat_records}") - length = len(chat_records) - # print(f"获取到的记录长度: {length}") - # 转换记录格式 - formatted_records = [] - for record in chat_records: - # 兼容行为,前向兼容老数据 - formatted_records.append( - { - "_id": record["_id"], - "time": record["time"], - "chat_id": record["chat_id"], - "detailed_plain_text": record.get("detailed_plain_text", ""), # 添加文本内容 - "memorized_times": record.get("memorized_times", 0), # 添加记忆次数 - } - ) - - return formatted_records - - return [] - async def get_recent_group_messages(chat_id: str, limit: int = 12) -> list: """从数据库获取群组最近的消息记录 diff --git a/src/plugins/chat/utils_image.py b/src/plugins/chat/utils_image.py index 78f6c5010..a1b699c29 100644 --- a/src/plugins/chat/utils_image.py +++ b/src/plugins/chat/utils_image.py @@ -9,7 +9,7 @@ import io from nonebot import get_driver from ...common.database import db -from ..chat.config import global_config +from ..config.config import global_config from ..models.utils_model import LLM_request from src.common.logger import get_module_logger diff --git a/src/plugins/chat/utils_user.py b/src/plugins/chat/utils_user.py index 973e7933d..8b4078411 100644 --- a/src/plugins/chat/utils_user.py +++ b/src/plugins/chat/utils_user.py @@ -1,4 +1,4 @@ -from .config import global_config +from ..config.config import global_config from .relationship_manager import relationship_manager diff --git a/src/plugins/chat/config.py b/src/plugins/config/config.py similarity index 100% rename from src/plugins/chat/config.py rename to src/plugins/config/config.py diff --git a/src/plugins/config/config_env.py b/src/plugins/config/config_env.py new file mode 100644 index 000000000..930e2c01c --- /dev/null +++ b/src/plugins/config/config_env.py @@ -0,0 +1,55 @@ +import os +from pathlib import Path +from dotenv import load_dotenv + +class EnvConfig: + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super(EnvConfig, cls).__new__(cls) + cls._instance._initialized = False + return cls._instance + + def __init__(self): + if self._initialized: + return + + self._initialized = True + self.ROOT_DIR = Path(__file__).parent.parent.parent.parent + self.load_env() + + def load_env(self): + env_file = self.ROOT_DIR / '.env' + if env_file.exists(): + load_dotenv(env_file) + + # 根据ENVIRONMENT变量加载对应的环境文件 + env_type = os.getenv('ENVIRONMENT', 'prod') + if env_type == 'dev': + env_file = self.ROOT_DIR / '.env.dev' + elif env_type == 'prod': + env_file = self.ROOT_DIR / '.env.prod' + + if env_file.exists(): + load_dotenv(env_file, override=True) + + def get(self, key, default=None): + return os.getenv(key, default) + + def get_all(self): + return dict(os.environ) + + def __getattr__(self, name): + return self.get(name) + +# 创建全局实例 +env_config = EnvConfig() + +# 导出环境变量 +def get_env(key, default=None): + return os.getenv(key, default) + +# 导出所有环境变量 +def get_all_env(): + return dict(os.environ) \ No newline at end of file diff --git a/src/plugins/memory_system/Hippocampus.py b/src/plugins/memory_system/Hippocampus.py index d8f976fc4..71956c3f1 100644 --- a/src/plugins/memory_system/Hippocampus.py +++ b/src/plugins/memory_system/Hippocampus.py @@ -6,19 +6,77 @@ import time import re import jieba import networkx as nx - -# from nonebot import get_driver +import numpy as np +from collections import Counter from ...common.database import db -# from ..chat.config import global_config -from ..chat.utils import ( - calculate_information_content, - cosine_similarity, - get_closest_chat_from_db, -) -from ..models.utils_model import LLM_request +from ...plugins.models.utils_model import LLM_request from src.common.logger import get_module_logger, LogConfig, MEMORY_STYLE_CONFIG from src.plugins.memory_system.sample_distribution import MemoryBuildScheduler #分布生成器 -from .config import MemoryConfig +from .memory_config import MemoryConfig + + +def get_closest_chat_from_db(length: int, timestamp: str): + # print(f"获取最接近指定时间戳的聊天记录,长度: {length}, 时间戳: {timestamp}") + # print(f"当前时间: {timestamp},转换后时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(timestamp))}") + chat_records = [] + closest_record = db.messages.find_one({"time": {"$lte": timestamp}}, sort=[("time", -1)]) + # print(f"最接近的记录: {closest_record}") + if closest_record: + closest_time = closest_record["time"] + chat_id = closest_record["chat_id"] # 获取chat_id + # 获取该时间戳之后的length条消息,保持相同的chat_id + chat_records = list( + db.messages.find( + { + "time": {"$gt": closest_time}, + "chat_id": chat_id, # 添加chat_id过滤 + } + ) + .sort("time", 1) + .limit(length) + ) + # print(f"获取到的记录: {chat_records}") + length = len(chat_records) + # print(f"获取到的记录长度: {length}") + # 转换记录格式 + formatted_records = [] + for record in chat_records: + # 兼容行为,前向兼容老数据 + formatted_records.append( + { + "_id": record["_id"], + "time": record["time"], + "chat_id": record["chat_id"], + "detailed_plain_text": record.get("detailed_plain_text", ""), # 添加文本内容 + "memorized_times": record.get("memorized_times", 0), # 添加记忆次数 + } + ) + + return formatted_records + + return [] + +def calculate_information_content(text): + """计算文本的信息量(熵)""" + char_count = Counter(text) + total_chars = len(text) + + entropy = 0 + for count in char_count.values(): + probability = count / total_chars + entropy -= probability * math.log2(probability) + + return entropy + +def cosine_similarity(v1, v2): + """计算余弦相似度""" + dot_product = np.dot(v1, v2) + norm1 = np.linalg.norm(v1) + norm2 = np.linalg.norm(v2) + if norm1 == 0 or norm2 == 0: + return 0 + return dot_product / (norm1 * norm2) + # 定义日志配置 memory_config = LogConfig( @@ -393,6 +451,59 @@ class EntorhinalCortex: if need_update: logger.success("[数据库] 已为缺失的时间字段进行补充") + async def resync_memory_to_db(self): + """清空数据库并重新同步所有记忆数据""" + start_time = time.time() + logger.info("[数据库] 开始重新同步所有记忆数据...") + + # 清空数据库 + clear_start = time.time() + db.graph_data.nodes.delete_many({}) + db.graph_data.edges.delete_many({}) + clear_end = time.time() + logger.info(f"[数据库] 清空数据库耗时: {clear_end - clear_start:.2f}秒") + + # 获取所有节点和边 + memory_nodes = list(self.memory_graph.G.nodes(data=True)) + memory_edges = list(self.memory_graph.G.edges(data=True)) + + # 重新写入节点 + node_start = time.time() + for concept, data in memory_nodes: + memory_items = data.get("memory_items", []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + + node_data = { + "concept": concept, + "memory_items": memory_items, + "hash": self.hippocampus.calculate_node_hash(concept, memory_items), + "created_time": data.get("created_time", datetime.datetime.now().timestamp()), + "last_modified": data.get("last_modified", datetime.datetime.now().timestamp()), + } + db.graph_data.nodes.insert_one(node_data) + node_end = time.time() + logger.info(f"[数据库] 写入 {len(memory_nodes)} 个节点耗时: {node_end - node_start:.2f}秒") + + # 重新写入边 + edge_start = time.time() + for source, target, data in memory_edges: + edge_data = { + "source": source, + "target": target, + "strength": data.get("strength", 1), + "hash": self.hippocampus.calculate_edge_hash(source, target), + "created_time": data.get("created_time", datetime.datetime.now().timestamp()), + "last_modified": data.get("last_modified", datetime.datetime.now().timestamp()), + } + db.graph_data.edges.insert_one(edge_data) + edge_end = time.time() + logger.info(f"[数据库] 写入 {len(memory_edges)} 条边耗时: {edge_end - edge_start:.2f}秒") + + end_time = time.time() + logger.success(f"[数据库] 重新同步完成,总耗时: {end_time - start_time:.2f}秒") + logger.success(f"[数据库] 同步了 {len(memory_nodes)} 个节点和 {len(memory_edges)} 条边") + #负责整合,遗忘,合并记忆 class ParahippocampalGyrus: def __init__(self, hippocampus): @@ -582,7 +693,8 @@ class ParahippocampalGyrus: "秒---------------------" ) - async def operation_forget_topic(self, percentage=0.1): + async def operation_forget_topic(self, percentage=0.005): + start_time = time.time() logger.info("[遗忘] 开始检查数据库...") all_nodes = list(self.memory_graph.G.nodes()) @@ -598,12 +710,20 @@ class ParahippocampalGyrus: nodes_to_check = random.sample(all_nodes, check_nodes_count) edges_to_check = random.sample(all_edges, check_edges_count) - edge_changes = {"weakened": 0, "removed": 0} - node_changes = {"reduced": 0, "removed": 0} + # 使用列表存储变化信息 + edge_changes = { + "weakened": [], # 存储减弱的边 + "removed": [] # 存储移除的边 + } + node_changes = { + "reduced": [], # 存储减少记忆的节点 + "removed": [] # 存储移除的节点 + } current_time = datetime.datetime.now().timestamp() logger.info("[遗忘] 开始检查连接...") + edge_check_start = time.time() for source, target in edges_to_check: edge_data = self.memory_graph.G[source][target] last_modified = edge_data.get("last_modified") @@ -614,15 +734,16 @@ class ParahippocampalGyrus: if new_strength <= 0: self.memory_graph.G.remove_edge(source, target) - edge_changes["removed"] += 1 - logger.info(f"[遗忘] 连接移除: {source} -> {target}") + edge_changes["removed"].append(f"{source} -> {target}") else: edge_data["strength"] = new_strength edge_data["last_modified"] = current_time - edge_changes["weakened"] += 1 - logger.info(f"[遗忘] 连接减弱: {source} -> {target} (强度: {current_strength} -> {new_strength})") + edge_changes["weakened"].append(f"{source}-{target} (强度: {current_strength} -> {new_strength})") + edge_check_end = time.time() + logger.info(f"[遗忘] 连接检查耗时: {edge_check_end - edge_check_start:.2f}秒") logger.info("[遗忘] 开始检查节点...") + node_check_start = time.time() for node in nodes_to_check: node_data = self.memory_graph.G.nodes[node] last_modified = node_data.get("last_modified", current_time) @@ -640,21 +761,40 @@ class ParahippocampalGyrus: if memory_items: self.memory_graph.G.nodes[node]["memory_items"] = memory_items self.memory_graph.G.nodes[node]["last_modified"] = current_time - node_changes["reduced"] += 1 - logger.info(f"[遗忘] 记忆减少: {node} (数量: {current_count} -> {len(memory_items)})") + node_changes["reduced"].append(f"{node} (数量: {current_count} -> {len(memory_items)})") else: self.memory_graph.G.remove_node(node) - node_changes["removed"] += 1 - logger.info(f"[遗忘] 节点移除: {node}") + node_changes["removed"].append(node) + node_check_end = time.time() + logger.info(f"[遗忘] 节点检查耗时: {node_check_end - node_check_start:.2f}秒") - if any(count > 0 for count in edge_changes.values()) or any(count > 0 for count in node_changes.values()): - await self.hippocampus.entorhinal_cortex.sync_memory_to_db() - logger.info("[遗忘] 统计信息:") - logger.info(f"[遗忘] 连接变化: {edge_changes['weakened']} 个减弱, {edge_changes['removed']} 个移除") - logger.info(f"[遗忘] 节点变化: {node_changes['reduced']} 个减少记忆, {node_changes['removed']} 个移除") + if any(edge_changes.values()) or any(node_changes.values()): + sync_start = time.time() + + await self.hippocampus.entorhinal_cortex.resync_memory_to_db() + + sync_end = time.time() + logger.info(f"[遗忘] 数据库同步耗时: {sync_end - sync_start:.2f}秒") + + # 汇总输出所有变化 + logger.info("[遗忘] 遗忘操作统计:") + if edge_changes["weakened"]: + logger.info(f"[遗忘] 减弱的连接 ({len(edge_changes['weakened'])}个): {', '.join(edge_changes['weakened'])}") + + if edge_changes["removed"]: + logger.info(f"[遗忘] 移除的连接 ({len(edge_changes['removed'])}个): {', '.join(edge_changes['removed'])}") + + if node_changes["reduced"]: + logger.info(f"[遗忘] 减少记忆的节点 ({len(node_changes['reduced'])}个): {', '.join(node_changes['reduced'])}") + + if node_changes["removed"]: + logger.info(f"[遗忘] 移除的节点 ({len(node_changes['removed'])}个): {', '.join(node_changes['removed'])}") else: logger.info("[遗忘] 本次检查没有节点或连接满足遗忘条件") + end_time = time.time() + logger.info(f"[遗忘] 总耗时: {end_time - start_time:.2f}秒") + # 海马体 class Hippocampus: def __init__(self): @@ -696,7 +836,7 @@ class Hippocampus: prompt = ( f"这是一段文字:{text}。请你从这段话中总结出最多{topic_num}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来," f"将主题用逗号隔开,并加上<>,例如<主题1>,<主题2>......尽可能精简。只需要列举最多{topic_num}个话题就好,不要有序号,不要告诉我其他内容。" - f"如果找不出主题或者没有明显主题,返回。" + f"如果确定找不出主题或者没有明显主题,返回。" ) return prompt @@ -763,7 +903,7 @@ class Hippocampus: memories.sort(key=lambda x: x[2], reverse=True) return memories - async def get_memory_from_text(self, text: str, num: int = 5, max_depth: int = 2, + async def get_memory_from_text(self, text: str, num: int = 5, max_depth: int = 3, fast_retrieval: bool = False) -> list: """从文本中提取关键词并获取相关记忆。 @@ -795,7 +935,8 @@ class Hippocampus: keywords = keywords[:5] else: # 使用LLM提取关键词 - topic_num = min(5, max(1, int(len(text) * 0.1))) # 根据文本长度动态调整关键词数量 + topic_num = min(5, max(1, int(len(text) * 0.2))) # 根据文本长度动态调整关键词数量 + print(f"提取关键词数量: {topic_num}") topics_response = await self.llm_topic_judge.generate_response( self.find_topic_llm(text, topic_num) ) @@ -811,11 +952,84 @@ class Hippocampus: if keyword.strip() ] + logger.info(f"提取的关键词: {', '.join(keywords)}") + # 从每个关键词获取记忆 all_memories = [] + keyword_connections = [] # 存储关键词之间的连接关系 + + # 检查关键词之间的连接 + for i in range(len(keywords)): + for j in range(i + 1, len(keywords)): + keyword1, keyword2 = keywords[i], keywords[j] + + # 检查节点是否存在于图中 + if keyword1 not in self.memory_graph.G or keyword2 not in self.memory_graph.G: + logger.debug(f"关键词 {keyword1} 或 {keyword2} 不在记忆图中") + continue + + # 检查直接连接 + if self.memory_graph.G.has_edge(keyword1, keyword2): + keyword_connections.append((keyword1, keyword2, 1)) + logger.info(f"发现直接连接: {keyword1} <-> {keyword2} (长度: 1)") + continue + + # 检查间接连接(通过其他节点) + for depth in range(2, max_depth + 1): + # 使用networkx的shortest_path_length检查是否存在指定长度的路径 + try: + path_length = nx.shortest_path_length(self.memory_graph.G, keyword1, keyword2) + if path_length <= depth: + keyword_connections.append((keyword1, keyword2, path_length)) + logger.info(f"发现间接连接: {keyword1} <-> {keyword2} (长度: {path_length})") + # 输出连接路径 + path = nx.shortest_path(self.memory_graph.G, keyword1, keyword2) + logger.info(f"连接路径: {' -> '.join(path)}") + break + except nx.NetworkXNoPath: + continue + + if not keyword_connections: + logger.info("未发现任何关键词之间的连接") + + # 记录已处理的关键词连接 + processed_connections = set() + + # 从每个关键词获取记忆 for keyword in keywords: - memories = self.get_memory_from_keyword(keyword, max_depth) - all_memories.extend(memories) + if keyword in self.memory_graph.G: # 只处理存在于图中的关键词 + memories = self.get_memory_from_keyword(keyword, max_depth) + all_memories.extend(memories) + + # 处理关键词连接相关的记忆 + for keyword1, keyword2, path_length in keyword_connections: + if (keyword1, keyword2) in processed_connections or (keyword2, keyword1) in processed_connections: + continue + + processed_connections.add((keyword1, keyword2)) + + # 获取连接路径上的所有节点 + try: + path = nx.shortest_path(self.memory_graph.G, keyword1, keyword2) + for node in path: + if node not in keywords: # 只处理路径上的非关键词节点 + node_data = self.memory_graph.G.nodes[node] + memory_items = node_data.get("memory_items", []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + + # 计算与输入文本的相似度 + node_words = set(jieba.cut(node)) + text_words = set(jieba.cut(text)) + all_words = node_words | text_words + v1 = [1 if word in node_words else 0 for word in all_words] + v2 = [1 if word in text_words else 0 for word in all_words] + similarity = cosine_similarity(v1, v2) + + if similarity >= 0.3: # 相似度阈值 + all_memories.append((node, memory_items, similarity)) + except nx.NetworkXNoPath: + continue # 去重(基于主题) seen_topics = set() @@ -871,6 +1085,16 @@ class HippocampusManager: logger.success(f"记忆构建分布: {config.memory_build_distribution}") logger.success("--------------------------------") + # 输出记忆图统计信息 + memory_graph = self._hippocampus.memory_graph.G + node_count = len(memory_graph.nodes()) + edge_count = len(memory_graph.edges()) + logger.success("--------------------------------") + logger.success("记忆图统计信息:") + logger.success(f"记忆节点数量: {node_count}") + logger.success(f"记忆连接数量: {edge_count}") + logger.success("--------------------------------") + return self._hippocampus async def build_memory(self): @@ -879,7 +1103,7 @@ class HippocampusManager: raise RuntimeError("HippocampusManager 尚未初始化,请先调用 initialize 方法") return await self._hippocampus.parahippocampal_gyrus.operation_build_memory() - async def forget_memory(self, percentage: float = 0.1): + async def forget_memory(self, percentage: float = 0.005): """遗忘记忆的公共接口""" if not self._initialized: raise RuntimeError("HippocampusManager 尚未初始化,请先调用 initialize 方法") diff --git a/src/plugins/memory_system/debug_memory.py b/src/plugins/memory_system/debug_memory.py new file mode 100644 index 000000000..e24e8c500 --- /dev/null +++ b/src/plugins/memory_system/debug_memory.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +import asyncio +import time +import sys +import os +# 添加项目根目录到系统路径 +sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))) +from src.plugins.memory_system.Hippocampus import HippocampusManager +from src.plugins.config.config import global_config + +async def test_memory_system(): + """测试记忆系统的主要功能""" + try: + # 初始化记忆系统 + print("开始初始化记忆系统...") + hippocampus_manager = HippocampusManager.get_instance() + hippocampus_manager.initialize(global_config=global_config) + print("记忆系统初始化完成") + + # 测试记忆构建 + # print("开始测试记忆构建...") + # await hippocampus_manager.build_memory() + # print("记忆构建完成") + + # 测试记忆检索 + test_text = "千石可乐在群里聊天" + test_text = '''[03-24 10:39:37] 麦麦(ta的id:2814567326): 早说散步结果下雨改成室内运动啊 +[03-24 10:39:37] 麦麦(ta的id:2814567326): [回复:变量] 变量就像今天计划总变 +[03-24 10:39:44] 状态异常(ta的id:535554838): 要把本地文件改成弹出来的路径吗 +[03-24 10:40:35] 状态异常(ta的id:535554838): [图片:这张图片显示的是Windows系统的环境变量设置界面。界面左侧列出了多个环境变量的值,包括Intel Dev Redist、Windows、Windows PowerShell、OpenSSH、NVIDIA Corporation的目录等。右侧有新建、编辑、浏览、删除、上移、下移和编辑文本等操作按钮。图片下方有一个错误提示框,显示"Windows找不到文件'mongodb\\bin\\mongod.exe'。请确定文件名是否正确后,再试一次。"这意味着用户试图运行MongoDB的mongod.exe程序时,系统找不到该文件。这可能是因为MongoDB的安装路径未正确添加到系统环境变量中,或者文件路径有误。 +图片的含义可能是用户正在尝试设置MongoDB的环境变量,以便在命令行或其他程序中使用MongoDB。如果用户正确设置了环境变量,那么他们应该能够通过命令行或其他方式启动MongoDB服务。] +[03-24 10:41:08] 一根猫(ta的id:108886006): [回复 麦麦 的消息: [回复某人消息] 改系统变量或者删库重配 ] [@麦麦] 我中途修改人格,需要重配吗 +[03-24 10:41:54] 麦麦(ta的id:2814567326): [回复:[回复 麦麦 的消息: [回复某人消息] 改系统变量或者删库重配 ] [@麦麦] 我中途修改人格,需要重配吗] 看情况 +[03-24 10:41:54] 麦麦(ta的id:2814567326): 难 +[03-24 10:41:54] 麦麦(ta的id:2814567326): 小改变量就行,大动骨安排重配像游戏副本南度改太大会崩 +[03-24 10:45:33] 霖泷(ta的id:1967075066): 话说现在思考高达一分钟 +[03-24 10:45:38] 霖泷(ta的id:1967075066): 是不是哪里出问题了 +[03-24 10:45:39] 艾卡(ta的id:1786525298): [表情包:这张表情包展示了一个动漫角色,她有着紫色的头发和大大的眼睛,表情显得有些困惑或不解。她的头上有一个问号,进一步强调了她的疑惑。整体情感表达的是困惑或不解。] +[03-24 10:46:12] (ta的id:3229291803): [表情包:这张表情包显示了一只手正在做"点赞"的动作,通常表示赞同、喜欢或支持。这个表情包所表达的情感是积极的、赞同的或支持的。] +[03-24 10:46:37] 星野風禾(ta的id:2890165435): 还能思考高达 +[03-24 10:46:39] 星野風禾(ta的id:2890165435): 什么知识库 +[03-24 10:46:49] ❦幻凌慌てない(ta的id:2459587037): 为什么改了回复系数麦麦还是不怎么回复?大佬们''' + + + test_text = '''千石可乐:niko分不清AI的陪伴和人类的陪伴,是这样吗?''' + print(f"开始测试记忆检索,测试文本: {test_text}\n") + memories = await hippocampus_manager.get_memory_from_text( + text=test_text, + num=3, + max_depth=3, + fast_retrieval=False + ) + + print("检索到的记忆:") + for topic, memory_items, similarity in memories: + print(f"主题: {topic}") + print(f"相似度: {similarity:.2f}") + for memory in memory_items: + print(f"- {memory}") + + + + # 测试记忆遗忘 + # forget_start_time = time.time() + # # print("开始测试记忆遗忘...") + # await hippocampus_manager.forget_memory(percentage=0.005) + # # print("记忆遗忘完成") + # forget_end_time = time.time() + # print(f"记忆遗忘耗时: {forget_end_time - forget_start_time:.2f} 秒") + + # 获取所有节点 + # nodes = hippocampus_manager.get_all_node_names() + # print(f"当前记忆系统中的节点数量: {len(nodes)}") + # print("节点列表:") + # for node in nodes: + # print(f"- {node}") + + except Exception as e: + print(f"测试过程中出现错误: {e}") + raise + +async def main(): + """主函数""" + try: + start_time = time.time() + await test_memory_system() + end_time = time.time() + print(f"测试完成,总耗时: {end_time - start_time:.2f} 秒") + except Exception as e: + print(f"程序执行出错: {e}") + raise + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/src/plugins/memory_system/draw_memory.py b/src/plugins/memory_system/draw_memory.py deleted file mode 100644 index 584985bbd..000000000 --- a/src/plugins/memory_system/draw_memory.py +++ /dev/null @@ -1,298 +0,0 @@ -# -*- coding: utf-8 -*- -import os -import sys -import time - -import jieba -import matplotlib.pyplot as plt -import networkx as nx -from dotenv import load_dotenv -from loguru import logger -# from src.common.logger import get_module_logger - -# logger = get_module_logger("draw_memory") - -# 添加项目根目录到 Python 路径 -root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) -sys.path.append(root_path) - -print(root_path) - -from src.common.database import db # noqa: E402 - -# 加载.env.dev文件 -env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), ".env.dev") -load_dotenv(env_path) - - -class Memory_graph: - def __init__(self): - self.G = nx.Graph() # 使用 networkx 的图结构 - - def connect_dot(self, concept1, concept2): - self.G.add_edge(concept1, concept2) - - def add_dot(self, concept, memory): - if concept in self.G: - # 如果节点已存在,将新记忆添加到现有列表中 - if "memory_items" in self.G.nodes[concept]: - if not isinstance(self.G.nodes[concept]["memory_items"], list): - # 如果当前不是列表,将其转换为列表 - self.G.nodes[concept]["memory_items"] = [self.G.nodes[concept]["memory_items"]] - self.G.nodes[concept]["memory_items"].append(memory) - else: - self.G.nodes[concept]["memory_items"] = [memory] - else: - # 如果是新节点,创建新的记忆列表 - self.G.add_node(concept, memory_items=[memory]) - - def get_dot(self, concept): - # 检查节点是否存在于图中 - if concept in self.G: - # 从图中获取节点数据 - node_data = self.G.nodes[concept] - # print(node_data) - # 创建新的Memory_dot对象 - return concept, node_data - return None - - def get_related_item(self, topic, depth=1): - if topic not in self.G: - return [], [] - - first_layer_items = [] - second_layer_items = [] - - # 获取相邻节点 - neighbors = list(self.G.neighbors(topic)) - # print(f"第一层: {topic}") - - # 获取当前节点的记忆项 - node_data = self.get_dot(topic) - if node_data: - concept, data = node_data - if "memory_items" in data: - memory_items = data["memory_items"] - if isinstance(memory_items, list): - first_layer_items.extend(memory_items) - else: - first_layer_items.append(memory_items) - - # 只在depth=2时获取第二层记忆 - if depth >= 2: - # 获取相邻节点的记忆项 - for neighbor in neighbors: - # print(f"第二层: {neighbor}") - node_data = self.get_dot(neighbor) - if node_data: - concept, data = node_data - if "memory_items" in data: - memory_items = data["memory_items"] - if isinstance(memory_items, list): - second_layer_items.extend(memory_items) - else: - second_layer_items.append(memory_items) - - return first_layer_items, second_layer_items - - def store_memory(self): - for node in self.G.nodes(): - dot_data = {"concept": node} - db.store_memory_dots.insert_one(dot_data) - - @property - def dots(self): - # 返回所有节点对应的 Memory_dot 对象 - return [self.get_dot(node) for node in self.G.nodes()] - - def get_random_chat_from_db(self, length: int, timestamp: str): - # 从数据库中根据时间戳获取离其最近的聊天记录 - chat_text = "" - closest_record = db.messages.find_one({"time": {"$lte": timestamp}}, sort=[("time", -1)]) # 调试输出 - logger.info( - f"距离time最近的消息时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(closest_record['time'])))}" - ) - - if closest_record: - closest_time = closest_record["time"] - group_id = closest_record["group_id"] # 获取groupid - # 获取该时间戳之后的length条消息,且groupid相同 - chat_record = list( - db.messages.find({"time": {"$gt": closest_time}, "group_id": group_id}).sort("time", 1).limit(length) - ) - for record in chat_record: - time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(record["time"]))) - try: - displayname = "[(%s)%s]%s" % (record["user_id"], record["user_nickname"], record["user_cardname"]) - except (KeyError, TypeError): - # 处理缺少键或类型错误的情况 - displayname = record.get("user_nickname", "") or "用户" + str(record.get("user_id", "未知")) - chat_text += f"[{time_str}] {displayname}: {record['processed_plain_text']}\n" # 添加发送者和时间信息 - return chat_text - - return [] # 如果没有找到记录,返回空列表 - - def save_graph_to_db(self): - # 清空现有的图数据 - db.graph_data.delete_many({}) - # 保存节点 - for node in self.G.nodes(data=True): - node_data = { - "concept": node[0], - "memory_items": node[1].get("memory_items", []), # 默认为空列表 - } - db.graph_data.nodes.insert_one(node_data) - # 保存边 - for edge in self.G.edges(): - edge_data = {"source": edge[0], "target": edge[1]} - db.graph_data.edges.insert_one(edge_data) - - def load_graph_from_db(self): - # 清空当前图 - self.G.clear() - # 加载节点 - nodes = db.graph_data.nodes.find() - for node in nodes: - memory_items = node.get("memory_items", []) - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - self.G.add_node(node["concept"], memory_items=memory_items) - # 加载边 - edges = db.graph_data.edges.find() - for edge in edges: - self.G.add_edge(edge["source"], edge["target"]) - - -def main(): - memory_graph = Memory_graph() - memory_graph.load_graph_from_db() - - # 只显示一次优化后的图形 - visualize_graph_lite(memory_graph) - - while True: - query = input("请输入新的查询概念(输入'退出'以结束):") - if query.lower() == "退出": - break - first_layer_items, second_layer_items = memory_graph.get_related_item(query) - if first_layer_items or second_layer_items: - logger.debug("第一层记忆:") - for item in first_layer_items: - logger.debug(item) - logger.debug("第二层记忆:") - for item in second_layer_items: - logger.debug(item) - else: - logger.debug("未找到相关记忆。") - - -def segment_text(text): - seg_text = list(jieba.cut(text)) - return seg_text - - -def find_topic(text, topic_num): - prompt = ( - f"这是一段文字:{text}。请你从这段话中总结出{topic_num}个话题,帮我列出来,用逗号隔开,尽可能精简。" - f"只需要列举{topic_num}个话题就好,不要告诉我其他内容。" - ) - return prompt - - -def topic_what(text, topic): - prompt = ( - f"这是一段文字:{text}。我想知道这记忆里有什么关于{topic}的话题,帮我总结成一句自然的话,可以包含时间和人物。" - f"只输出这句话就好" - ) - return prompt - - -def visualize_graph_lite(memory_graph: Memory_graph, color_by_memory: bool = False): - # 设置中文字体 - plt.rcParams["font.sans-serif"] = ["SimHei"] # 用来正常显示中文标签 - plt.rcParams["axes.unicode_minus"] = False # 用来正常显示负号 - - G = memory_graph.G - - # 创建一个新图用于可视化 - H = G.copy() - - # 移除只有一条记忆的节点和连接数少于3的节点 - nodes_to_remove = [] - for node in H.nodes(): - memory_items = H.nodes[node].get("memory_items", []) - memory_count = len(memory_items) if isinstance(memory_items, list) else (1 if memory_items else 0) - degree = H.degree(node) - if memory_count < 3 or degree < 2: # 改为小于2而不是小于等于2 - nodes_to_remove.append(node) - - H.remove_nodes_from(nodes_to_remove) - - # 如果过滤后没有节点,则返回 - if len(H.nodes()) == 0: - logger.debug("过滤后没有符合条件的节点可显示") - return - - # 保存图到本地 - # nx.write_gml(H, "memory_graph.gml") # 保存为 GML 格式 - - # 计算节点大小和颜色 - node_colors = [] - node_sizes = [] - nodes = list(H.nodes()) - - # 获取最大记忆数和最大度数用于归一化 - max_memories = 1 - max_degree = 1 - for node in nodes: - memory_items = H.nodes[node].get("memory_items", []) - memory_count = len(memory_items) if isinstance(memory_items, list) else (1 if memory_items else 0) - degree = H.degree(node) - max_memories = max(max_memories, memory_count) - max_degree = max(max_degree, degree) - - # 计算每个节点的大小和颜色 - for node in nodes: - # 计算节点大小(基于记忆数量) - memory_items = H.nodes[node].get("memory_items", []) - memory_count = len(memory_items) if isinstance(memory_items, list) else (1 if memory_items else 0) - # 使用指数函数使变化更明显 - ratio = memory_count / max_memories - size = 500 + 5000 * (ratio) # 使用1.5次方函数使差异不那么明显 - node_sizes.append(size) - - # 计算节点颜色(基于连接数) - degree = H.degree(node) - # 红色分量随着度数增加而增加 - r = (degree / max_degree) ** 0.3 - red = min(1.0, r) - # 蓝色分量随着度数减少而增加 - blue = max(0.0, 1 - red) - # blue = 1 - color = (red, 0.1, blue) - node_colors.append(color) - - # 绘制图形 - plt.figure(figsize=(12, 8)) - pos = nx.spring_layout(H, k=1, iterations=50) # 增加k值使节点分布更开 - nx.draw( - H, - pos, - with_labels=True, - node_color=node_colors, - node_size=node_sizes, - font_size=10, - font_family="SimHei", - font_weight="bold", - edge_color="gray", - width=0.5, - alpha=0.9, - ) - - title = "记忆图谱可视化 - 节点大小表示记忆数量,颜色表示连接数" - plt.title(title, fontsize=16, fontfamily="SimHei") - plt.show() - - -if __name__ == "__main__": - main() diff --git a/src/plugins/memory_system/config.py b/src/plugins/memory_system/memory_config.py similarity index 100% rename from src/plugins/memory_system/config.py rename to src/plugins/memory_system/memory_config.py diff --git a/src/plugins/memory_system/memory_deprecated.py b/src/plugins/memory_system/memory_deprecated.py deleted file mode 100644 index 3760b2ac9..000000000 --- a/src/plugins/memory_system/memory_deprecated.py +++ /dev/null @@ -1,1006 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -import math -import random -import time -import re - -import jieba -import networkx as nx - -from nonebot import get_driver -from ...common.database import db -from ..chat.config import global_config -from ..chat.utils import ( - calculate_information_content, - cosine_similarity, - get_closest_chat_from_db, - text_to_vector, -) -from ..models.utils_model import LLM_request -from src.common.logger import get_module_logger, LogConfig, MEMORY_STYLE_CONFIG -from src.plugins.memory_system.sample_distribution import MemoryBuildScheduler - -# 定义日志配置 -memory_config = LogConfig( - # 使用海马体专用样式 - console_format=MEMORY_STYLE_CONFIG["console_format"], - file_format=MEMORY_STYLE_CONFIG["file_format"], -) - - -logger = get_module_logger("memory_system", config=memory_config) - - -class Memory_graph: - def __init__(self): - self.G = nx.Graph() # 使用 networkx 的图结构 - - def connect_dot(self, concept1, concept2): - # 避免自连接 - if concept1 == concept2: - return - - current_time = datetime.datetime.now().timestamp() - - # 如果边已存在,增加 strength - if self.G.has_edge(concept1, concept2): - self.G[concept1][concept2]["strength"] = self.G[concept1][concept2].get("strength", 1) + 1 - # 更新最后修改时间 - self.G[concept1][concept2]["last_modified"] = current_time - else: - # 如果是新边,初始化 strength 为 1 - self.G.add_edge( - concept1, - concept2, - strength=1, - created_time=current_time, # 添加创建时间 - last_modified=current_time, - ) # 添加最后修改时间 - - def add_dot(self, concept, memory): - current_time = datetime.datetime.now().timestamp() - - if concept in self.G: - if "memory_items" in self.G.nodes[concept]: - if not isinstance(self.G.nodes[concept]["memory_items"], list): - self.G.nodes[concept]["memory_items"] = [self.G.nodes[concept]["memory_items"]] - self.G.nodes[concept]["memory_items"].append(memory) - # 更新最后修改时间 - self.G.nodes[concept]["last_modified"] = current_time - else: - self.G.nodes[concept]["memory_items"] = [memory] - # 如果节点存在但没有memory_items,说明是第一次添加memory,设置created_time - if "created_time" not in self.G.nodes[concept]: - self.G.nodes[concept]["created_time"] = current_time - self.G.nodes[concept]["last_modified"] = current_time - else: - # 如果是新节点,创建新的记忆列表 - self.G.add_node( - concept, - memory_items=[memory], - created_time=current_time, # 添加创建时间 - last_modified=current_time, - ) # 添加最后修改时间 - - def get_dot(self, concept): - # 检查节点是否存在于图中 - if concept in self.G: - # 从图中获取节点数据 - node_data = self.G.nodes[concept] - return concept, node_data - return None - - def get_related_item(self, topic, depth=1): - if topic not in self.G: - return [], [] - - first_layer_items = [] - second_layer_items = [] - - # 获取相邻节点 - neighbors = list(self.G.neighbors(topic)) - - # 获取当前节点的记忆项 - node_data = self.get_dot(topic) - if node_data: - concept, data = node_data - if "memory_items" in data: - memory_items = data["memory_items"] - if isinstance(memory_items, list): - first_layer_items.extend(memory_items) - else: - first_layer_items.append(memory_items) - - # 只在depth=2时获取第二层记忆 - if depth >= 2: - # 获取相邻节点的记忆项 - for neighbor in neighbors: - node_data = self.get_dot(neighbor) - if node_data: - concept, data = node_data - if "memory_items" in data: - memory_items = data["memory_items"] - if isinstance(memory_items, list): - second_layer_items.extend(memory_items) - else: - second_layer_items.append(memory_items) - - return first_layer_items, second_layer_items - - @property - def dots(self): - # 返回所有节点对应的 Memory_dot 对象 - return [self.get_dot(node) for node in self.G.nodes()] - - def forget_topic(self, topic): - """随机删除指定话题中的一条记忆,如果话题没有记忆则移除该话题节点""" - if topic not in self.G: - return None - - # 获取话题节点数据 - node_data = self.G.nodes[topic] - - # 如果节点存在memory_items - if "memory_items" in node_data: - memory_items = node_data["memory_items"] - - # 确保memory_items是列表 - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - - # 如果有记忆项可以删除 - if memory_items: - # 随机选择一个记忆项删除 - removed_item = random.choice(memory_items) - memory_items.remove(removed_item) - - # 更新节点的记忆项 - if memory_items: - self.G.nodes[topic]["memory_items"] = memory_items - else: - # 如果没有记忆项了,删除整个节点 - self.G.remove_node(topic) - - return removed_item - - return None - - -# 海马体 -class Hippocampus: - def __init__(self, memory_graph: Memory_graph): - self.memory_graph = memory_graph - self.llm_topic_judge = LLM_request(model=global_config.llm_topic_judge, temperature=0.5, request_type="memory") - self.llm_summary_by_topic = LLM_request( - model=global_config.llm_summary_by_topic, temperature=0.5, request_type="memory" - ) - - def get_all_node_names(self) -> list: - """获取记忆图中所有节点的名字列表 - - Returns: - list: 包含所有节点名字的列表 - """ - return list(self.memory_graph.G.nodes()) - - def calculate_node_hash(self, concept, memory_items): - """计算节点的特征值""" - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - sorted_items = sorted(memory_items) - content = f"{concept}:{'|'.join(sorted_items)}" - return hash(content) - - def calculate_edge_hash(self, source, target): - """计算边的特征值""" - nodes = sorted([source, target]) - return hash(f"{nodes[0]}:{nodes[1]}") - - def random_get_msg_snippet(self, target_timestamp: float, chat_size: int, max_memorized_time_per_msg: int) -> list: - try_count = 0 - # 最多尝试2次抽取 - while try_count < 3: - messages = get_closest_chat_from_db(length=chat_size, timestamp=target_timestamp) - if messages: - # print(f"抽取到的消息: {messages}") - # 检查messages是否均没有达到记忆次数限制 - for message in messages: - if message["memorized_times"] >= max_memorized_time_per_msg: - messages = None - # print(f"抽取到的消息提取次数达到限制,跳过") - break - if messages: - # 成功抽取短期消息样本 - # 数据写回:增加记忆次数 - for message in messages: - db.messages.update_one( - {"_id": message["_id"]}, {"$set": {"memorized_times": message["memorized_times"] + 1}} - ) - return messages - try_count += 1 - return None - - def get_memory_sample(self): - # 硬编码:每条消息最大记忆次数 - # 如有需求可写入global_config - max_memorized_time_per_msg = 3 - - # 创建双峰分布的记忆调度器 - sample_scheduler = MemoryBuildScheduler( - n_hours1=global_config.memory_build_distribution[0], # 第一个分布均值(4小时前) - std_hours1=global_config.memory_build_distribution[1], # 第一个分布标准差 - weight1=global_config.memory_build_distribution[2], # 第一个分布权重 60% - n_hours2=global_config.memory_build_distribution[3], # 第二个分布均值(24小时前) - std_hours2=global_config.memory_build_distribution[4], # 第二个分布标准差 - weight2=global_config.memory_build_distribution[5], # 第二个分布权重 40% - total_samples=global_config.build_memory_sample_num # 总共生成10个时间点 - ) - - # 生成时间戳数组 - timestamps = sample_scheduler.get_timestamp_array() - # logger.debug(f"生成的时间戳数组: {timestamps}") - # print(f"生成的时间戳数组: {timestamps}") - # print(f"时间戳的实际时间: {[time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(ts)) for ts in timestamps]}") - logger.info(f"回忆往事: {[time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(ts)) for ts in timestamps]}") - chat_samples = [] - for timestamp in timestamps: - messages = self.random_get_msg_snippet( - timestamp, - global_config.build_memory_sample_length, - max_memorized_time_per_msg - ) - if messages: - time_diff = (datetime.datetime.now().timestamp() - timestamp) / 3600 - logger.debug(f"成功抽取 {time_diff:.1f} 小时前的消息样本,共{len(messages)}条") - # print(f"成功抽取 {time_diff:.1f} 小时前的消息样本,共{len(messages)}条") - chat_samples.append(messages) - else: - logger.debug(f"时间戳 {timestamp} 的消息样本抽取失败") - - return chat_samples - - async def memory_compress(self, messages: list, compress_rate=0.1): - if not messages: - return set(), {} - - # 合并消息文本,同时保留时间信息 - input_text = "" - time_info = "" - # 计算最早和最晚时间 - earliest_time = min(msg["time"] for msg in messages) - latest_time = max(msg["time"] for msg in messages) - - earliest_dt = datetime.datetime.fromtimestamp(earliest_time) - latest_dt = datetime.datetime.fromtimestamp(latest_time) - - # 如果是同一年 - if earliest_dt.year == latest_dt.year: - earliest_str = earliest_dt.strftime("%m-%d %H:%M:%S") - latest_str = latest_dt.strftime("%m-%d %H:%M:%S") - time_info += f"是在{earliest_dt.year}年,{earliest_str} 到 {latest_str} 的对话:\n" - else: - earliest_str = earliest_dt.strftime("%Y-%m-%d %H:%M:%S") - latest_str = latest_dt.strftime("%Y-%m-%d %H:%M:%S") - time_info += f"是从 {earliest_str} 到 {latest_str} 的对话:\n" - - for msg in messages: - input_text += f"{msg['detailed_plain_text']}\n" - - logger.debug(input_text) - - topic_num = self.calculate_topic_num(input_text, compress_rate) - topics_response = await self.llm_topic_judge.generate_response(self.find_topic_llm(input_text, topic_num)) - - # 使用正则表达式提取<>中的内容 - topics = re.findall(r'<([^>]+)>', topics_response[0]) - - # 如果没有找到<>包裹的内容,返回['none'] - if not topics: - topics = ['none'] - else: - # 处理提取出的话题 - topics = [ - topic.strip() - for topic in ','.join(topics).replace(",", ",").replace("、", ",").replace(" ", ",").split(",") - if topic.strip() - ] - - # 过滤掉包含禁用关键词的topic - # any()检查topic中是否包含任何一个filter_keywords中的关键词 - # 只保留不包含禁用关键词的topic - filtered_topics = [ - topic for topic in topics - if not any(keyword in topic for keyword in global_config.memory_ban_words) - ] - - logger.debug(f"过滤后话题: {filtered_topics}") - - # 创建所有话题的请求任务 - tasks = [] - for topic in filtered_topics: - topic_what_prompt = self.topic_what(input_text, topic, time_info) - task = self.llm_summary_by_topic.generate_response_async(topic_what_prompt) - tasks.append((topic.strip(), task)) - - # 等待所有任务完成 - # 初始化压缩后的记忆集合和相似主题字典 - compressed_memory = set() # 存储压缩后的(主题,内容)元组 - similar_topics_dict = {} # 存储每个话题的相似主题列表 - - # 遍历每个主题及其对应的LLM任务 - for topic, task in tasks: - response = await task - if response: - # 将主题和LLM生成的内容添加到压缩记忆中 - compressed_memory.add((topic, response[0])) - - # 为当前主题寻找相似的已存在主题 - existing_topics = list(self.memory_graph.G.nodes()) - similar_topics = [] - - # 计算当前主题与每个已存在主题的相似度 - for existing_topic in existing_topics: - # 使用jieba分词,将主题转换为词集合 - topic_words = set(jieba.cut(topic)) - existing_words = set(jieba.cut(existing_topic)) - - # 构建词向量用于计算余弦相似度 - all_words = topic_words | existing_words # 所有不重复的词 - v1 = [1 if word in topic_words else 0 for word in all_words] # 当前主题的词向量 - v2 = [1 if word in existing_words else 0 for word in all_words] # 已存在主题的词向量 - - # 计算余弦相似度 - similarity = cosine_similarity(v1, v2) - - # 如果相似度超过阈值,添加到相似主题列表 - if similarity >= 0.7: - similar_topics.append((existing_topic, similarity)) - - # 按相似度降序排序,只保留前3个最相似的主题 - similar_topics.sort(key=lambda x: x[1], reverse=True) - similar_topics = similar_topics[:3] - similar_topics_dict[topic] = similar_topics - - return compressed_memory, similar_topics_dict - - def calculate_topic_num(self, text, compress_rate): - """计算文本的话题数量""" - information_content = calculate_information_content(text) - topic_by_length = text.count("\n") * compress_rate - topic_by_information_content = max(1, min(5, int((information_content - 3) * 2))) - topic_num = int((topic_by_length + topic_by_information_content) / 2) - logger.debug( - f"topic_by_length: {topic_by_length}, topic_by_information_content: {topic_by_information_content}, " - f"topic_num: {topic_num}" - ) - return topic_num - - async def operation_build_memory(self): - logger.debug("------------------------------------开始构建记忆--------------------------------------") - start_time = time.time() - memory_samples = self.get_memory_sample() - all_added_nodes = [] - all_connected_nodes = [] - all_added_edges = [] - for i, messages in enumerate(memory_samples, 1): - all_topics = [] - # 加载进度可视化 - progress = (i / len(memory_samples)) * 100 - bar_length = 30 - filled_length = int(bar_length * i // len(memory_samples)) - bar = "█" * filled_length + "-" * (bar_length - filled_length) - logger.debug(f"进度: [{bar}] {progress:.1f}% ({i}/{len(memory_samples)})") - - compress_rate = global_config.memory_compress_rate - compressed_memory, similar_topics_dict = await self.memory_compress(messages, compress_rate) - logger.debug(f"压缩后记忆数量: {compressed_memory},似曾相识的话题: {similar_topics_dict}") - - current_time = datetime.datetime.now().timestamp() - logger.debug(f"添加节点: {', '.join(topic for topic, _ in compressed_memory)}") - all_added_nodes.extend(topic for topic, _ in compressed_memory) - # all_connected_nodes.extend(topic for topic, _ in similar_topics_dict) - - for topic, memory in compressed_memory: - self.memory_graph.add_dot(topic, memory) - all_topics.append(topic) - - # 连接相似的已存在主题 - if topic in similar_topics_dict: - similar_topics = similar_topics_dict[topic] - for similar_topic, similarity in similar_topics: - if topic != similar_topic: - strength = int(similarity * 10) - - logger.debug(f"连接相似节点: {topic} 和 {similar_topic} (强度: {strength})") - all_added_edges.append(f"{topic}-{similar_topic}") - - all_connected_nodes.append(topic) - all_connected_nodes.append(similar_topic) - - self.memory_graph.G.add_edge( - topic, - similar_topic, - strength=strength, - created_time=current_time, - last_modified=current_time, - ) - - # 连接同批次的相关话题 - for i in range(len(all_topics)): - for j in range(i + 1, len(all_topics)): - logger.debug(f"连接同批次节点: {all_topics[i]} 和 {all_topics[j]}") - all_added_edges.append(f"{all_topics[i]}-{all_topics[j]}") - self.memory_graph.connect_dot(all_topics[i], all_topics[j]) - - logger.success(f"更新记忆: {', '.join(all_added_nodes)}") - logger.debug(f"强化连接: {', '.join(all_added_edges)}") - logger.info(f"强化连接节点: {', '.join(all_connected_nodes)}") - # logger.success(f"强化连接: {', '.join(all_added_edges)}") - self.sync_memory_to_db() - - end_time = time.time() - logger.success( - f"--------------------------记忆构建完成:耗时: {end_time - start_time:.2f} " - "秒--------------------------" - ) - - def sync_memory_to_db(self): - """检查并同步内存中的图结构与数据库""" - # 获取数据库中所有节点和内存中所有节点 - db_nodes = list(db.graph_data.nodes.find()) - memory_nodes = list(self.memory_graph.G.nodes(data=True)) - - # 转换数据库节点为字典格式,方便查找 - db_nodes_dict = {node["concept"]: node for node in db_nodes} - - # 检查并更新节点 - for concept, data in memory_nodes: - memory_items = data.get("memory_items", []) - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - - # 计算内存中节点的特征值 - memory_hash = self.calculate_node_hash(concept, memory_items) - - # 获取时间信息 - created_time = data.get("created_time", datetime.datetime.now().timestamp()) - last_modified = data.get("last_modified", datetime.datetime.now().timestamp()) - - if concept not in db_nodes_dict: - # 数据库中缺少的节点,添加 - node_data = { - "concept": concept, - "memory_items": memory_items, - "hash": memory_hash, - "created_time": created_time, - "last_modified": last_modified, - } - db.graph_data.nodes.insert_one(node_data) - else: - # 获取数据库中节点的特征值 - db_node = db_nodes_dict[concept] - db_hash = db_node.get("hash", None) - - # 如果特征值不同,则更新节点 - if db_hash != memory_hash: - db.graph_data.nodes.update_one( - {"concept": concept}, - { - "$set": { - "memory_items": memory_items, - "hash": memory_hash, - "created_time": created_time, - "last_modified": last_modified, - } - }, - ) - - # 处理边的信息 - db_edges = list(db.graph_data.edges.find()) - memory_edges = list(self.memory_graph.G.edges(data=True)) - - # 创建边的哈希值字典 - db_edge_dict = {} - for edge in db_edges: - edge_hash = self.calculate_edge_hash(edge["source"], edge["target"]) - db_edge_dict[(edge["source"], edge["target"])] = {"hash": edge_hash, "strength": edge.get("strength", 1)} - - # 检查并更新边 - for source, target, data in memory_edges: - edge_hash = self.calculate_edge_hash(source, target) - edge_key = (source, target) - strength = data.get("strength", 1) - - # 获取边的时间信息 - created_time = data.get("created_time", datetime.datetime.now().timestamp()) - last_modified = data.get("last_modified", datetime.datetime.now().timestamp()) - - if edge_key not in db_edge_dict: - # 添加新边 - edge_data = { - "source": source, - "target": target, - "strength": strength, - "hash": edge_hash, - "created_time": created_time, - "last_modified": last_modified, - } - db.graph_data.edges.insert_one(edge_data) - else: - # 检查边的特征值是否变化 - if db_edge_dict[edge_key]["hash"] != edge_hash: - db.graph_data.edges.update_one( - {"source": source, "target": target}, - { - "$set": { - "hash": edge_hash, - "strength": strength, - "created_time": created_time, - "last_modified": last_modified, - } - }, - ) - - def sync_memory_from_db(self): - """从数据库同步数据到内存中的图结构""" - current_time = datetime.datetime.now().timestamp() - need_update = False - - # 清空当前图 - self.memory_graph.G.clear() - - # 从数据库加载所有节点 - nodes = list(db.graph_data.nodes.find()) - for node in nodes: - concept = node["concept"] - memory_items = node.get("memory_items", []) - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - - # 检查时间字段是否存在 - if "created_time" not in node or "last_modified" not in node: - need_update = True - # 更新数据库中的节点 - update_data = {} - if "created_time" not in node: - update_data["created_time"] = current_time - if "last_modified" not in node: - update_data["last_modified"] = current_time - - db.graph_data.nodes.update_one({"concept": concept}, {"$set": update_data}) - logger.info(f"[时间更新] 节点 {concept} 添加缺失的时间字段") - - # 获取时间信息(如果不存在则使用当前时间) - created_time = node.get("created_time", current_time) - last_modified = node.get("last_modified", current_time) - - # 添加节点到图中 - self.memory_graph.G.add_node( - concept, memory_items=memory_items, created_time=created_time, last_modified=last_modified - ) - - # 从数据库加载所有边 - edges = list(db.graph_data.edges.find()) - for edge in edges: - source = edge["source"] - target = edge["target"] - strength = edge.get("strength", 1) - - # 检查时间字段是否存在 - if "created_time" not in edge or "last_modified" not in edge: - need_update = True - # 更新数据库中的边 - update_data = {} - if "created_time" not in edge: - update_data["created_time"] = current_time - if "last_modified" not in edge: - update_data["last_modified"] = current_time - - db.graph_data.edges.update_one({"source": source, "target": target}, {"$set": update_data}) - logger.info(f"[时间更新] 边 {source} - {target} 添加缺失的时间字段") - - # 获取时间信息(如果不存在则使用当前时间) - created_time = edge.get("created_time", current_time) - last_modified = edge.get("last_modified", current_time) - - # 只有当源节点和目标节点都存在时才添加边 - if source in self.memory_graph.G and target in self.memory_graph.G: - self.memory_graph.G.add_edge( - source, target, strength=strength, created_time=created_time, last_modified=last_modified - ) - - if need_update: - logger.success("[数据库] 已为缺失的时间字段进行补充") - - async def operation_forget_topic(self, percentage=0.1): - """随机选择图中一定比例的节点和边进行检查,根据时间条件决定是否遗忘""" - # 检查数据库是否为空 - # logger.remove() - - logger.info("[遗忘] 开始检查数据库... 当前Logger信息:") - # logger.info(f"- Logger名称: {logger.name}") - # logger.info(f"- Logger等级: {logger.level}") - # logger.info(f"- Logger处理器: {[handler.__class__.__name__ for handler in logger.handlers]}") - - # logger2 = setup_logger(LogModule.MEMORY) - # logger2.info(f"[遗忘] 开始检查数据库... 当前Logger信息:") - # logger.info(f"[遗忘] 开始检查数据库... 当前Logger信息:") - - all_nodes = list(self.memory_graph.G.nodes()) - all_edges = list(self.memory_graph.G.edges()) - - if not all_nodes and not all_edges: - logger.info("[遗忘] 记忆图为空,无需进行遗忘操作") - return - - check_nodes_count = max(1, int(len(all_nodes) * percentage)) - check_edges_count = max(1, int(len(all_edges) * percentage)) - - nodes_to_check = random.sample(all_nodes, check_nodes_count) - edges_to_check = random.sample(all_edges, check_edges_count) - - edge_changes = {"weakened": 0, "removed": 0} - node_changes = {"reduced": 0, "removed": 0} - - current_time = datetime.datetime.now().timestamp() - - # 检查并遗忘连接 - logger.info("[遗忘] 开始检查连接...") - for source, target in edges_to_check: - edge_data = self.memory_graph.G[source][target] - last_modified = edge_data.get("last_modified") - - if current_time - last_modified > 3600 * global_config.memory_forget_time: - current_strength = edge_data.get("strength", 1) - new_strength = current_strength - 1 - - if new_strength <= 0: - self.memory_graph.G.remove_edge(source, target) - edge_changes["removed"] += 1 - logger.info(f"[遗忘] 连接移除: {source} -> {target}") - else: - edge_data["strength"] = new_strength - edge_data["last_modified"] = current_time - edge_changes["weakened"] += 1 - logger.info(f"[遗忘] 连接减弱: {source} -> {target} (强度: {current_strength} -> {new_strength})") - - # 检查并遗忘话题 - logger.info("[遗忘] 开始检查节点...") - for node in nodes_to_check: - node_data = self.memory_graph.G.nodes[node] - last_modified = node_data.get("last_modified", current_time) - - if current_time - last_modified > 3600 * 24: - memory_items = node_data.get("memory_items", []) - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - - if memory_items: - current_count = len(memory_items) - removed_item = random.choice(memory_items) - memory_items.remove(removed_item) - - if memory_items: - self.memory_graph.G.nodes[node]["memory_items"] = memory_items - self.memory_graph.G.nodes[node]["last_modified"] = current_time - node_changes["reduced"] += 1 - logger.info(f"[遗忘] 记忆减少: {node} (数量: {current_count} -> {len(memory_items)})") - else: - self.memory_graph.G.remove_node(node) - node_changes["removed"] += 1 - logger.info(f"[遗忘] 节点移除: {node}") - - if any(count > 0 for count in edge_changes.values()) or any(count > 0 for count in node_changes.values()): - self.sync_memory_to_db() - logger.info("[遗忘] 统计信息:") - logger.info(f"[遗忘] 连接变化: {edge_changes['weakened']} 个减弱, {edge_changes['removed']} 个移除") - logger.info(f"[遗忘] 节点变化: {node_changes['reduced']} 个减少记忆, {node_changes['removed']} 个移除") - else: - logger.info("[遗忘] 本次检查没有节点或连接满足遗忘条件") - - async def merge_memory(self, topic): - """对指定话题的记忆进行合并压缩""" - # 获取节点的记忆项 - memory_items = self.memory_graph.G.nodes[topic].get("memory_items", []) - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - - # 如果记忆项不足,直接返回 - if len(memory_items) < 10: - return - - # 随机选择10条记忆 - selected_memories = random.sample(memory_items, 10) - - # 拼接成文本 - merged_text = "\n".join(selected_memories) - logger.debug(f"[合并] 话题: {topic}") - logger.debug(f"[合并] 选择的记忆:\n{merged_text}") - - # 使用memory_compress生成新的压缩记忆 - compressed_memories, _ = await self.memory_compress(selected_memories, 0.1) - - # 从原记忆列表中移除被选中的记忆 - for memory in selected_memories: - memory_items.remove(memory) - - # 添加新的压缩记忆 - for _, compressed_memory in compressed_memories: - memory_items.append(compressed_memory) - logger.info(f"[合并] 添加压缩记忆: {compressed_memory}") - - # 更新节点的记忆项 - self.memory_graph.G.nodes[topic]["memory_items"] = memory_items - logger.debug(f"[合并] 完成记忆合并,当前记忆数量: {len(memory_items)}") - - async def operation_merge_memory(self, percentage=0.1): - """ - 随机检查一定比例的节点,对内容数量超过100的节点进行记忆合并 - - Args: - percentage: 要检查的节点比例,默认为0.1(10%) - """ - # 获取所有节点 - all_nodes = list(self.memory_graph.G.nodes()) - # 计算要检查的节点数量 - check_count = max(1, int(len(all_nodes) * percentage)) - # 随机选择节点 - nodes_to_check = random.sample(all_nodes, check_count) - - merged_nodes = [] - for node in nodes_to_check: - # 获取节点的内容条数 - memory_items = self.memory_graph.G.nodes[node].get("memory_items", []) - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - content_count = len(memory_items) - - # 如果内容数量超过100,进行合并 - if content_count > 100: - logger.debug(f"检查节点: {node}, 当前记忆数量: {content_count}") - await self.merge_memory(node) - merged_nodes.append(node) - - # 同步到数据库 - if merged_nodes: - self.sync_memory_to_db() - logger.debug(f"完成记忆合并操作,共处理 {len(merged_nodes)} 个节点") - else: - logger.debug("本次检查没有需要合并的节点") - - def find_topic_llm(self, text, topic_num): - prompt = ( - f"这是一段文字:{text}。请你从这段话中总结出最多{topic_num}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来," - f"将主题用逗号隔开,并加上<>,例如<主题1>,<主题2>......尽可能精简。只需要列举最多{topic_num}个话题就好,不要有序号,不要告诉我其他内容。" - f"如果找不出主题或者没有明显主题,返回。" - ) - return prompt - - def topic_what(self, text, topic, time_info): - prompt = ( - f'这是一段文字,{time_info}:{text}。我想让你基于这段文字来概括"{topic}"这个概念,帮我总结成一句自然的话,' - f"可以包含时间和人物,以及具体的观点。只输出这句话就好" - ) - return prompt - - async def _identify_topics(self, text: str) -> list: - """从文本中识别可能的主题 - - Args: - text: 输入文本 - - Returns: - list: 识别出的主题列表 - """ - topics_response = await self.llm_topic_judge.generate_response(self.find_topic_llm(text, 4)) - # 使用正则表达式提取<>中的内容 - # print(f"话题: {topics_response[0]}") - topics = re.findall(r'<([^>]+)>', topics_response[0]) - - # 如果没有找到<>包裹的内容,返回['none'] - if not topics: - topics = ['none'] - else: - # 处理提取出的话题 - topics = [ - topic.strip() - for topic in ','.join(topics).replace(",", ",").replace("、", ",").replace(" ", ",").split(",") - if topic.strip() - ] - - return topics - - def _find_similar_topics(self, topics: list, similarity_threshold: float = 0.4, debug_info: str = "") -> list: - """查找与给定主题相似的记忆主题 - - Args: - topics: 主题列表 - similarity_threshold: 相似度阈值 - debug_info: 调试信息前缀 - - Returns: - list: (主题, 相似度) 元组列表 - """ - all_memory_topics = self.get_all_node_names() - all_similar_topics = [] - - # 计算每个识别出的主题与记忆主题的相似度 - for topic in topics: - if debug_info: - # print(f"\033[1;32m[{debug_info}]\033[0m 正在思考有没有见过: {topic}") - pass - - topic_vector = text_to_vector(topic) - has_similar_topic = False - - for memory_topic in all_memory_topics: - memory_vector = text_to_vector(memory_topic) - # 获取所有唯一词 - all_words = set(topic_vector.keys()) | set(memory_vector.keys()) - # 构建向量 - v1 = [topic_vector.get(word, 0) for word in all_words] - v2 = [memory_vector.get(word, 0) for word in all_words] - # 计算相似度 - similarity = cosine_similarity(v1, v2) - - if similarity >= similarity_threshold: - has_similar_topic = True - if debug_info: - pass - all_similar_topics.append((memory_topic, similarity)) - - if not has_similar_topic and debug_info: - # print(f"\033[1;31m[{debug_info}]\033[0m 没有见过: {topic} ,呃呃") - pass - - return all_similar_topics - - def _get_top_topics(self, similar_topics: list, max_topics: int = 5) -> list: - """获取相似度最高的主题 - - Args: - similar_topics: (主题, 相似度) 元组列表 - max_topics: 最大主题数量 - - Returns: - list: (主题, 相似度) 元组列表 - """ - seen_topics = set() - top_topics = [] - - for topic, score in sorted(similar_topics, key=lambda x: x[1], reverse=True): - if topic not in seen_topics and len(top_topics) < max_topics: - seen_topics.add(topic) - top_topics.append((topic, score)) - - return top_topics - - async def memory_activate_value(self, text: str, max_topics: int = 5, similarity_threshold: float = 0.3) -> int: - """计算输入文本对记忆的激活程度""" - # 识别主题 - identified_topics = await self._identify_topics(text) - # print(f"识别主题: {identified_topics}") - - if identified_topics[0] == "none": - return 0 - - # 查找相似主题 - all_similar_topics = self._find_similar_topics( - identified_topics, similarity_threshold=similarity_threshold, debug_info="激活" - ) - - if not all_similar_topics: - return 0 - - # 获取最相关的主题 - top_topics = self._get_top_topics(all_similar_topics, max_topics) - - # 如果只找到一个主题,进行惩罚 - if len(top_topics) == 1: - topic, score = top_topics[0] - # 获取主题内容数量并计算惩罚系数 - memory_items = self.memory_graph.G.nodes[topic].get("memory_items", []) - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - content_count = len(memory_items) - penalty = 1.0 / (1 + math.log(content_count + 1)) - - activation = int(score * 50 * penalty) - logger.info(f"单主题「{topic}」- 相似度: {score:.3f}, 内容数: {content_count}, 激活值: {activation}") - return activation - - # 计算关键词匹配率,同时考虑内容数量 - matched_topics = set() - topic_similarities = {} - - for memory_topic, _similarity in top_topics: - # 计算内容数量惩罚 - memory_items = self.memory_graph.G.nodes[memory_topic].get("memory_items", []) - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - content_count = len(memory_items) - penalty = 1.0 / (1 + math.log(content_count + 1)) - - # 对每个记忆主题,检查它与哪些输入主题相似 - for input_topic in identified_topics: - topic_vector = text_to_vector(input_topic) - memory_vector = text_to_vector(memory_topic) - all_words = set(topic_vector.keys()) | set(memory_vector.keys()) - v1 = [topic_vector.get(word, 0) for word in all_words] - v2 = [memory_vector.get(word, 0) for word in all_words] - sim = cosine_similarity(v1, v2) - if sim >= similarity_threshold: - matched_topics.add(input_topic) - adjusted_sim = sim * penalty - topic_similarities[input_topic] = max(topic_similarities.get(input_topic, 0), adjusted_sim) - # logger.debug( - - # 计算主题匹配率和平均相似度 - topic_match = len(matched_topics) / len(identified_topics) - average_similarities = sum(topic_similarities.values()) / len(topic_similarities) if topic_similarities else 0 - - # 计算最终激活值 - activation = int((topic_match + average_similarities) / 2 * 100) - - logger.info(f"识别<{text[:15]}...>主题: {identified_topics}, 匹配率: {topic_match:.3f}, 激活值: {activation}") - - return activation - - async def get_relevant_memories( - self, text: str, max_topics: int = 5, similarity_threshold: float = 0.4, max_memory_num: int = 5 - ) -> list: - """根据输入文本获取相关的记忆内容""" - # 识别主题 - identified_topics = await self._identify_topics(text) - - # 查找相似主题 - all_similar_topics = self._find_similar_topics( - identified_topics, similarity_threshold=similarity_threshold, debug_info="记忆检索" - ) - - # 获取最相关的主题 - relevant_topics = self._get_top_topics(all_similar_topics, max_topics) - - # 获取相关记忆内容 - relevant_memories = [] - for topic, score in relevant_topics: - # 获取该主题的记忆内容 - first_layer, _ = self.memory_graph.get_related_item(topic, depth=1) - if first_layer: - # 如果记忆条数超过限制,随机选择指定数量的记忆 - if len(first_layer) > max_memory_num / 2: - first_layer = random.sample(first_layer, max_memory_num // 2) - # 为每条记忆添加来源主题和相似度信息 - for memory in first_layer: - relevant_memories.append({"topic": topic, "similarity": score, "content": memory}) - - # 如果记忆数量超过5个,随机选择5个 - # 按相似度排序 - relevant_memories.sort(key=lambda x: x["similarity"], reverse=True) - - if len(relevant_memories) > max_memory_num: - relevant_memories = random.sample(relevant_memories, max_memory_num) - - return relevant_memories - - -def segment_text(text): - seg_text = list(jieba.cut(text)) - return seg_text - - -driver = get_driver() -config = driver.config - -start_time = time.time() - -# 创建记忆图 -memory_graph = Memory_graph() -# 创建海马体 -hippocampus = Hippocampus(memory_graph) -# 从数据库加载记忆图 -hippocampus.sync_memory_from_db() - -end_time = time.time() -logger.success(f"加载海马体耗时: {end_time - start_time:.2f} 秒") diff --git a/src/plugins/memory_system/memory_manual_build.py b/src/plugins/memory_system/memory_manual_build.py deleted file mode 100644 index 4b5d3b155..000000000 --- a/src/plugins/memory_system/memory_manual_build.py +++ /dev/null @@ -1,992 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -import math -import os -import random -import sys -import time -from collections import Counter -from pathlib import Path -import matplotlib.pyplot as plt -import networkx as nx -from dotenv import load_dotenv -sys.path.insert(0, sys.path[0]+"/../") -sys.path.insert(0, sys.path[0]+"/../") -sys.path.insert(0, sys.path[0]+"/../") -sys.path.insert(0, sys.path[0]+"/../") -sys.path.insert(0, sys.path[0]+"/../") -from src.common.logger import get_module_logger -import jieba - -# from chat.config import global_config -# 添加项目根目录到 Python 路径 -root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) -sys.path.append(root_path) - -from src.common.database import db # noqa E402 -from src.plugins.memory_system.offline_llm import LLMModel # noqa E402 - -# 获取当前文件的目录 -current_dir = Path(__file__).resolve().parent -# 获取项目根目录(上三层目录) -project_root = current_dir.parent.parent.parent -# env.dev文件路径 -env_path = project_root / ".env.dev" - -logger = get_module_logger("mem_manual_bd") - -# 加载环境变量 -if env_path.exists(): - logger.info(f"从 {env_path} 加载环境变量") - load_dotenv(env_path) -else: - logger.warning(f"未找到环境变量文件: {env_path}") - logger.info("将使用默认配置") - - -def calculate_information_content(text): - """计算文本的信息量(熵)""" - char_count = Counter(text) - total_chars = len(text) - - entropy = 0 - for count in char_count.values(): - probability = count / total_chars - entropy -= probability * math.log2(probability) - - return entropy - - -def get_closest_chat_from_db(length: int, timestamp: str): - """从数据库中获取最接近指定时间戳的聊天记录,并记录读取次数 - - Returns: - list: 消息记录字典列表,每个字典包含消息内容和时间信息 - """ - chat_records = [] - closest_record = db.messages.find_one({"time": {"$lte": timestamp}}, sort=[("time", -1)]) - - if closest_record and closest_record.get("memorized", 0) < 4: - closest_time = closest_record["time"] - group_id = closest_record["group_id"] - # 获取该时间戳之后的length条消息,且groupid相同 - records = list( - db.messages.find({"time": {"$gt": closest_time}, "group_id": group_id}).sort("time", 1).limit(length) - ) - - # 更新每条消息的memorized属性 - for record in records: - current_memorized = record.get("memorized", 0) - if current_memorized > 3: - print("消息已读取3次,跳过") - return "" - - # 更新memorized值 - db.messages.update_one({"_id": record["_id"]}, {"$set": {"memorized": current_memorized + 1}}) - - # 添加到记录列表中 - chat_records.append( - {"text": record["detailed_plain_text"], "time": record["time"], "group_id": record["group_id"]} - ) - - return chat_records - - -class Memory_graph: - def __init__(self): - self.G = nx.Graph() # 使用 networkx 的图结构 - - def connect_dot(self, concept1, concept2): - # 如果边已存在,增加 strength - if self.G.has_edge(concept1, concept2): - self.G[concept1][concept2]["strength"] = self.G[concept1][concept2].get("strength", 1) + 1 - else: - # 如果是新边,初始化 strength 为 1 - self.G.add_edge(concept1, concept2, strength=1) - - def add_dot(self, concept, memory): - if concept in self.G: - # 如果节点已存在,将新记忆添加到现有列表中 - if "memory_items" in self.G.nodes[concept]: - if not isinstance(self.G.nodes[concept]["memory_items"], list): - # 如果当前不是列表,将其转换为列表 - self.G.nodes[concept]["memory_items"] = [self.G.nodes[concept]["memory_items"]] - self.G.nodes[concept]["memory_items"].append(memory) - else: - self.G.nodes[concept]["memory_items"] = [memory] - else: - # 如果是新节点,创建新的记忆列表 - self.G.add_node(concept, memory_items=[memory]) - - def get_dot(self, concept): - # 检查节点是否存在于图中 - if concept in self.G: - # 从图中获取节点数据 - node_data = self.G.nodes[concept] - return concept, node_data - return None - - def get_related_item(self, topic, depth=1): - if topic not in self.G: - return [], [] - - first_layer_items = [] - second_layer_items = [] - - # 获取相邻节点 - neighbors = list(self.G.neighbors(topic)) - - # 获取当前节点的记忆项 - node_data = self.get_dot(topic) - if node_data: - concept, data = node_data - if "memory_items" in data: - memory_items = data["memory_items"] - if isinstance(memory_items, list): - first_layer_items.extend(memory_items) - else: - first_layer_items.append(memory_items) - - # 只在depth=2时获取第二层记忆 - if depth >= 2: - # 获取相邻节点的记忆项 - for neighbor in neighbors: - node_data = self.get_dot(neighbor) - if node_data: - concept, data = node_data - if "memory_items" in data: - memory_items = data["memory_items"] - if isinstance(memory_items, list): - second_layer_items.extend(memory_items) - else: - second_layer_items.append(memory_items) - - return first_layer_items, second_layer_items - - @property - def dots(self): - # 返回所有节点对应的 Memory_dot 对象 - return [self.get_dot(node) for node in self.G.nodes()] - - -# 海马体 -class Hippocampus: - def __init__(self, memory_graph: Memory_graph): - self.memory_graph = memory_graph - self.llm_model = LLMModel() - self.llm_model_small = LLMModel(model_name="deepseek-ai/DeepSeek-V2.5") - self.llm_model_get_topic = LLMModel(model_name="Pro/Qwen/Qwen2.5-7B-Instruct") - self.llm_model_summary = LLMModel(model_name="Qwen/Qwen2.5-32B-Instruct") - - def get_memory_sample(self, chat_size=20, time_frequency=None): - """获取记忆样本 - - Returns: - list: 消息记录列表,每个元素是一个消息记录字典列表 - """ - if time_frequency is None: - time_frequency = {"near": 2, "mid": 4, "far": 3} - current_timestamp = datetime.datetime.now().timestamp() - chat_samples = [] - - # 短期:1h 中期:4h 长期:24h - for _ in range(time_frequency.get("near")): - random_time = current_timestamp - random.randint(1, 3600 * 4) - messages = get_closest_chat_from_db(length=chat_size, timestamp=random_time) - if messages: - chat_samples.append(messages) - - for _ in range(time_frequency.get("mid")): - random_time = current_timestamp - random.randint(3600 * 4, 3600 * 24) - messages = get_closest_chat_from_db(length=chat_size, timestamp=random_time) - if messages: - chat_samples.append(messages) - - for _ in range(time_frequency.get("far")): - random_time = current_timestamp - random.randint(3600 * 24, 3600 * 24 * 7) - messages = get_closest_chat_from_db(length=chat_size, timestamp=random_time) - if messages: - chat_samples.append(messages) - - return chat_samples - - def calculate_topic_num(self, text, compress_rate): - """计算文本的话题数量""" - information_content = calculate_information_content(text) - topic_by_length = text.count("\n") * compress_rate - topic_by_information_content = max(1, min(5, int((information_content - 3) * 2))) - topic_num = int((topic_by_length + topic_by_information_content) / 2) - print( - f"topic_by_length: {topic_by_length}, topic_by_information_content: {topic_by_information_content}, " - f"topic_num: {topic_num}" - ) - return topic_num - - async def memory_compress(self, messages: list, compress_rate=0.1): - """压缩消息记录为记忆 - - Args: - messages: 消息记录字典列表,每个字典包含text和time字段 - compress_rate: 压缩率 - - Returns: - set: (话题, 记忆) 元组集合 - """ - if not messages: - return set() - - # 合并消息文本,同时保留时间信息 - input_text = "" - time_info = "" - # 计算最早和最晚时间 - earliest_time = min(msg["time"] for msg in messages) - latest_time = max(msg["time"] for msg in messages) - - earliest_dt = datetime.datetime.fromtimestamp(earliest_time) - latest_dt = datetime.datetime.fromtimestamp(latest_time) - - # 如果是同一年 - if earliest_dt.year == latest_dt.year: - earliest_str = earliest_dt.strftime("%m-%d %H:%M:%S") - latest_str = latest_dt.strftime("%m-%d %H:%M:%S") - time_info += f"是在{earliest_dt.year}年,{earliest_str} 到 {latest_str} 的对话:\n" - else: - earliest_str = earliest_dt.strftime("%Y-%m-%d %H:%M:%S") - latest_str = latest_dt.strftime("%Y-%m-%d %H:%M:%S") - time_info += f"是从 {earliest_str} 到 {latest_str} 的对话:\n" - - for msg in messages: - input_text += f"{msg['text']}\n" - - print(input_text) - - topic_num = self.calculate_topic_num(input_text, compress_rate) - topics_response = self.llm_model_get_topic.generate_response(self.find_topic_llm(input_text, topic_num)) - - # 过滤topics - filter_keywords = ["表情包", "图片", "回复", "聊天记录"] - topics = [ - topic.strip() - for topic in topics_response[0].replace(",", ",").replace("、", ",").replace(" ", ",").split(",") - if topic.strip() - ] - filtered_topics = [topic for topic in topics if not any(keyword in topic for keyword in filter_keywords)] - - # print(f"原始话题: {topics}") - print(f"过滤后话题: {filtered_topics}") - - # 创建所有话题的请求任务 - tasks = [] - for topic in filtered_topics: - topic_what_prompt = self.topic_what(input_text, topic, time_info) - # 创建异步任务 - task = self.llm_model_small.generate_response_async(topic_what_prompt) - tasks.append((topic.strip(), task)) - - # 等待所有任务完成 - compressed_memory = set() - for topic, task in tasks: - response = await task - if response: - compressed_memory.add((topic, response[0])) - - return compressed_memory - - async def operation_build_memory(self, chat_size=12): - # 最近消息获取频率 - time_frequency = {"near": 3, "mid": 8, "far": 5} - memory_samples = self.get_memory_sample(chat_size, time_frequency) - - all_topics = [] # 用于存储所有话题 - - for i, messages in enumerate(memory_samples, 1): - # 加载进度可视化 - all_topics = [] - progress = (i / len(memory_samples)) * 100 - bar_length = 30 - filled_length = int(bar_length * i // len(memory_samples)) - bar = "█" * filled_length + "-" * (bar_length - filled_length) - print(f"\n进度: [{bar}] {progress:.1f}% ({i}/{len(memory_samples)})") - - # 生成压缩后记忆 - compress_rate = 0.1 - compressed_memory = await self.memory_compress(messages, compress_rate) - print(f"\033[1;33m压缩后记忆数量\033[0m: {len(compressed_memory)}") - - # 将记忆加入到图谱中 - for topic, memory in compressed_memory: - print(f"\033[1;32m添加节点\033[0m: {topic}") - self.memory_graph.add_dot(topic, memory) - all_topics.append(topic) - - # 连接相关话题 - for i in range(len(all_topics)): - for j in range(i + 1, len(all_topics)): - print(f"\033[1;32m连接节点\033[0m: {all_topics[i]} 和 {all_topics[j]}") - self.memory_graph.connect_dot(all_topics[i], all_topics[j]) - - self.sync_memory_to_db() - - def sync_memory_from_db(self): - """ - 从数据库同步数据到内存中的图结构 - 将清空当前内存中的图,并从数据库重新加载所有节点和边 - """ - # 清空当前图 - self.memory_graph.G.clear() - - # 从数据库加载所有节点 - nodes = db.graph_data.nodes.find() - for node in nodes: - concept = node["concept"] - memory_items = node.get("memory_items", []) - # 确保memory_items是列表 - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - # 添加节点到图中 - self.memory_graph.G.add_node(concept, memory_items=memory_items) - - # 从数据库加载所有边 - edges = db.graph_data.edges.find() - for edge in edges: - source = edge["source"] - target = edge["target"] - strength = edge.get("strength", 1) # 获取 strength,默认为 1 - # 只有当源节点和目标节点都存在时才添加边 - if source in self.memory_graph.G and target in self.memory_graph.G: - self.memory_graph.G.add_edge(source, target, strength=strength) - - logger.success("从数据库同步记忆图谱完成") - - def calculate_node_hash(self, concept, memory_items): - """ - 计算节点的特征值 - """ - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - # 将记忆项排序以确保相同内容生成相同的哈希值 - sorted_items = sorted(memory_items) - # 组合概念和记忆项生成特征值 - content = f"{concept}:{'|'.join(sorted_items)}" - return hash(content) - - def calculate_edge_hash(self, source, target): - """ - 计算边的特征值 - """ - # 对源节点和目标节点排序以确保相同的边生成相同的哈希值 - nodes = sorted([source, target]) - return hash(f"{nodes[0]}:{nodes[1]}") - - def sync_memory_to_db(self): - """ - 检查并同步内存中的图结构与数据库 - 使用特征值(哈希值)快速判断是否需要更新 - """ - # 获取数据库中所有节点和内存中所有节点 - db_nodes = list(db.graph_data.nodes.find()) - memory_nodes = list(self.memory_graph.G.nodes(data=True)) - - # 转换数据库节点为字典格式,方便查找 - db_nodes_dict = {node["concept"]: node for node in db_nodes} - - # 检查并更新节点 - for concept, data in memory_nodes: - memory_items = data.get("memory_items", []) - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - - # 计算内存中节点的特征值 - memory_hash = self.calculate_node_hash(concept, memory_items) - - if concept not in db_nodes_dict: - # 数据库中缺少的节点,添加 - # logger.info(f"添加新节点: {concept}") - node_data = {"concept": concept, "memory_items": memory_items, "hash": memory_hash} - db.graph_data.nodes.insert_one(node_data) - else: - # 获取数据库中节点的特征值 - db_node = db_nodes_dict[concept] - db_hash = db_node.get("hash", None) - - # 如果特征值不同,则更新节点 - if db_hash != memory_hash: - # logger.info(f"更新节点内容: {concept}") - db.graph_data.nodes.update_one( - {"concept": concept}, {"$set": {"memory_items": memory_items, "hash": memory_hash}} - ) - - # 检查并删除数据库中多余的节点 - memory_concepts = set(node[0] for node in memory_nodes) - for db_node in db_nodes: - if db_node["concept"] not in memory_concepts: - # logger.info(f"删除多余节点: {db_node['concept']}") - db.graph_data.nodes.delete_one({"concept": db_node["concept"]}) - - # 处理边的信息 - db_edges = list(db.graph_data.edges.find()) - memory_edges = list(self.memory_graph.G.edges()) - - # 创建边的哈希值字典 - db_edge_dict = {} - for edge in db_edges: - edge_hash = self.calculate_edge_hash(edge["source"], edge["target"]) - db_edge_dict[(edge["source"], edge["target"])] = {"hash": edge_hash, "num": edge.get("num", 1)} - - # 检查并更新边 - for source, target in memory_edges: - edge_hash = self.calculate_edge_hash(source, target) - edge_key = (source, target) - - if edge_key not in db_edge_dict: - # 添加新边 - logger.info(f"添加新边: {source} - {target}") - edge_data = {"source": source, "target": target, "num": 1, "hash": edge_hash} - db.graph_data.edges.insert_one(edge_data) - else: - # 检查边的特征值是否变化 - if db_edge_dict[edge_key]["hash"] != edge_hash: - logger.info(f"更新边: {source} - {target}") - db.graph_data.edges.update_one({"source": source, "target": target}, {"$set": {"hash": edge_hash}}) - - # 删除多余的边 - memory_edge_set = set(memory_edges) - for edge_key in db_edge_dict: - if edge_key not in memory_edge_set: - source, target = edge_key - logger.info(f"删除多余边: {source} - {target}") - db.graph_data.edges.delete_one({"source": source, "target": target}) - - logger.success("完成记忆图谱与数据库的差异同步") - - def find_topic_llm(self, text, topic_num): - prompt = ( - f"这是一段文字:{text}。请你从这段话中总结出{topic_num}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来," - f"用逗号,隔开,尽可能精简。只需要列举{topic_num}个话题就好,不要有序号,不要告诉我其他内容。" - ) - return prompt - - def topic_what(self, text, topic, time_info): - # 获取当前时间 - prompt = ( - f'这是一段文字,{time_info}:{text}。我想让你基于这段文字来概括"{topic}"这个概念,帮我总结成一句自然的话,' - f"可以包含时间和人物,以及具体的观点。只输出这句话就好" - ) - return prompt - - def remove_node_from_db(self, topic): - """ - 从数据库中删除指定节点及其相关的边 - - Args: - topic: 要删除的节点概念 - """ - # 删除节点 - db.graph_data.nodes.delete_one({"concept": topic}) - # 删除所有涉及该节点的边 - db.graph_data.edges.delete_many({"$or": [{"source": topic}, {"target": topic}]}) - - def forget_topic(self, topic): - """ - 随机删除指定话题中的一条记忆,如果话题没有记忆则移除该话题节点 - 只在内存中的图上操作,不直接与数据库交互 - - Args: - topic: 要删除记忆的话题 - - Returns: - removed_item: 被删除的记忆项,如果没有删除任何记忆则返回 None - """ - if topic not in self.memory_graph.G: - return None - - # 获取话题节点数据 - node_data = self.memory_graph.G.nodes[topic] - - # 如果节点存在memory_items - if "memory_items" in node_data: - memory_items = node_data["memory_items"] - - # 确保memory_items是列表 - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - - # 如果有记忆项可以删除 - if memory_items: - # 随机选择一个记忆项删除 - removed_item = random.choice(memory_items) - memory_items.remove(removed_item) - - # 更新节点的记忆项 - if memory_items: - self.memory_graph.G.nodes[topic]["memory_items"] = memory_items - else: - # 如果没有记忆项了,删除整个节点 - self.memory_graph.G.remove_node(topic) - - return removed_item - - return None - - async def operation_forget_topic(self, percentage=0.1): - """ - 随机选择图中一定比例的节点进行检查,根据条件决定是否遗忘 - - Args: - percentage: 要检查的节点比例,默认为0.1(10%) - """ - # 获取所有节点 - all_nodes = list(self.memory_graph.G.nodes()) - # 计算要检查的节点数量 - check_count = max(1, int(len(all_nodes) * percentage)) - # 随机选择节点 - nodes_to_check = random.sample(all_nodes, check_count) - - forgotten_nodes = [] - for node in nodes_to_check: - # 获取节点的连接数 - connections = self.memory_graph.G.degree(node) - - # 获取节点的内容条数 - memory_items = self.memory_graph.G.nodes[node].get("memory_items", []) - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - content_count = len(memory_items) - - # 检查连接强度 - weak_connections = True - if connections > 1: # 只有当连接数大于1时才检查强度 - for neighbor in self.memory_graph.G.neighbors(node): - strength = self.memory_graph.G[node][neighbor].get("strength", 1) - if strength > 2: - weak_connections = False - break - - # 如果满足遗忘条件 - if (connections <= 1 and weak_connections) or content_count <= 2: - removed_item = self.forget_topic(node) - if removed_item: - forgotten_nodes.append((node, removed_item)) - logger.info(f"遗忘节点 {node} 的记忆: {removed_item}") - - # 同步到数据库 - if forgotten_nodes: - self.sync_memory_to_db() - logger.info(f"完成遗忘操作,共遗忘 {len(forgotten_nodes)} 个节点的记忆") - else: - logger.info("本次检查没有节点满足遗忘条件") - - async def merge_memory(self, topic): - """ - 对指定话题的记忆进行合并压缩 - - Args: - topic: 要合并的话题节点 - """ - # 获取节点的记忆项 - memory_items = self.memory_graph.G.nodes[topic].get("memory_items", []) - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - - # 如果记忆项不足,直接返回 - if len(memory_items) < 10: - return - - # 随机选择10条记忆 - selected_memories = random.sample(memory_items, 10) - - # 拼接成文本 - merged_text = "\n".join(selected_memories) - print(f"\n[合并记忆] 话题: {topic}") - print(f"选择的记忆:\n{merged_text}") - - # 使用memory_compress生成新的压缩记忆 - compressed_memories = await self.memory_compress(selected_memories, 0.1) - - # 从原记忆列表中移除被选中的记忆 - for memory in selected_memories: - memory_items.remove(memory) - - # 添加新的压缩记忆 - for _, compressed_memory in compressed_memories: - memory_items.append(compressed_memory) - print(f"添加压缩记忆: {compressed_memory}") - - # 更新节点的记忆项 - self.memory_graph.G.nodes[topic]["memory_items"] = memory_items - print(f"完成记忆合并,当前记忆数量: {len(memory_items)}") - - async def operation_merge_memory(self, percentage=0.1): - """ - 随机检查一定比例的节点,对内容数量超过100的节点进行记忆合并 - - Args: - percentage: 要检查的节点比例,默认为0.1(10%) - """ - # 获取所有节点 - all_nodes = list(self.memory_graph.G.nodes()) - # 计算要检查的节点数量 - check_count = max(1, int(len(all_nodes) * percentage)) - # 随机选择节点 - nodes_to_check = random.sample(all_nodes, check_count) - - merged_nodes = [] - for node in nodes_to_check: - # 获取节点的内容条数 - memory_items = self.memory_graph.G.nodes[node].get("memory_items", []) - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - content_count = len(memory_items) - - # 如果内容数量超过100,进行合并 - if content_count > 100: - print(f"\n检查节点: {node}, 当前记忆数量: {content_count}") - await self.merge_memory(node) - merged_nodes.append(node) - - # 同步到数据库 - if merged_nodes: - self.sync_memory_to_db() - print(f"\n完成记忆合并操作,共处理 {len(merged_nodes)} 个节点") - else: - print("\n本次检查没有需要合并的节点") - - async def _identify_topics(self, text: str) -> list: - """从文本中识别可能的主题""" - topics_response = self.llm_model_get_topic.generate_response(self.find_topic_llm(text, 5)) - topics = [ - topic.strip() - for topic in topics_response[0].replace(",", ",").replace("、", ",").replace(" ", ",").split(",") - if topic.strip() - ] - return topics - - def _find_similar_topics(self, topics: list, similarity_threshold: float = 0.4, debug_info: str = "") -> list: - """查找与给定主题相似的记忆主题""" - all_memory_topics = list(self.memory_graph.G.nodes()) - all_similar_topics = [] - - for topic in topics: - if debug_info: - pass - - topic_vector = text_to_vector(topic) - - for memory_topic in all_memory_topics: - memory_vector = text_to_vector(memory_topic) - all_words = set(topic_vector.keys()) | set(memory_vector.keys()) - v1 = [topic_vector.get(word, 0) for word in all_words] - v2 = [memory_vector.get(word, 0) for word in all_words] - similarity = cosine_similarity(v1, v2) - - if similarity >= similarity_threshold: - all_similar_topics.append((memory_topic, similarity)) - - return all_similar_topics - - def _get_top_topics(self, similar_topics: list, max_topics: int = 5) -> list: - """获取相似度最高的主题""" - seen_topics = set() - top_topics = [] - - for topic, score in sorted(similar_topics, key=lambda x: x[1], reverse=True): - if topic not in seen_topics and len(top_topics) < max_topics: - seen_topics.add(topic) - top_topics.append((topic, score)) - - return top_topics - - async def memory_activate_value(self, text: str, max_topics: int = 5, similarity_threshold: float = 0.3) -> int: - """计算输入文本对记忆的激活程度""" - logger.info(f"[记忆激活]识别主题: {await self._identify_topics(text)}") - - identified_topics = await self._identify_topics(text) - if not identified_topics: - return 0 - - all_similar_topics = self._find_similar_topics( - identified_topics, similarity_threshold=similarity_threshold, debug_info="记忆激活" - ) - - if not all_similar_topics: - return 0 - - top_topics = self._get_top_topics(all_similar_topics, max_topics) - - if len(top_topics) == 1: - topic, score = top_topics[0] - memory_items = self.memory_graph.G.nodes[topic].get("memory_items", []) - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - content_count = len(memory_items) - penalty = 1.0 / (1 + math.log(content_count + 1)) - - activation = int(score * 50 * penalty) - print( - f"\033[1;32m[记忆激活]\033[0m 单主题「{topic}」- 相似度: {score:.3f}, 内容数: {content_count}, " - f"激活值: {activation}" - ) - return activation - - matched_topics = set() - topic_similarities = {} - - for memory_topic, _similarity in top_topics: - memory_items = self.memory_graph.G.nodes[memory_topic].get("memory_items", []) - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] - content_count = len(memory_items) - penalty = 1.0 / (1 + math.log(content_count + 1)) - - for input_topic in identified_topics: - topic_vector = text_to_vector(input_topic) - memory_vector = text_to_vector(memory_topic) - all_words = set(topic_vector.keys()) | set(memory_vector.keys()) - v1 = [topic_vector.get(word, 0) for word in all_words] - v2 = [memory_vector.get(word, 0) for word in all_words] - sim = cosine_similarity(v1, v2) - if sim >= similarity_threshold: - matched_topics.add(input_topic) - adjusted_sim = sim * penalty - topic_similarities[input_topic] = max(topic_similarities.get(input_topic, 0), adjusted_sim) - print( - f"\033[1;32m[记忆激活]\033[0m 主题「{input_topic}」-> " - f"「{memory_topic}」(内容数: {content_count}, " - f"相似度: {adjusted_sim:.3f})" - ) - - topic_match = len(matched_topics) / len(identified_topics) - average_similarities = sum(topic_similarities.values()) / len(topic_similarities) if topic_similarities else 0 - - activation = int((topic_match + average_similarities) / 2 * 100) - print( - f"\033[1;32m[记忆激活]\033[0m 匹配率: {topic_match:.3f}, 平均相似度: {average_similarities:.3f}, " - f"激活值: {activation}" - ) - - return activation - - async def get_relevant_memories( - self, text: str, max_topics: int = 5, similarity_threshold: float = 0.4, max_memory_num: int = 5 - ) -> list: - """根据输入文本获取相关的记忆内容""" - identified_topics = await self._identify_topics(text) - - all_similar_topics = self._find_similar_topics( - identified_topics, similarity_threshold=similarity_threshold, debug_info="记忆检索" - ) - - relevant_topics = self._get_top_topics(all_similar_topics, max_topics) - - relevant_memories = [] - for topic, score in relevant_topics: - first_layer, _ = self.memory_graph.get_related_item(topic, depth=1) - if first_layer: - if len(first_layer) > max_memory_num / 2: - first_layer = random.sample(first_layer, max_memory_num // 2) - for memory in first_layer: - relevant_memories.append({"topic": topic, "similarity": score, "content": memory}) - - relevant_memories.sort(key=lambda x: x["similarity"], reverse=True) - - if len(relevant_memories) > max_memory_num: - relevant_memories = random.sample(relevant_memories, max_memory_num) - - return relevant_memories - - -def segment_text(text): - """使用jieba进行文本分词""" - seg_text = list(jieba.cut(text)) - return seg_text - - -def text_to_vector(text): - """将文本转换为词频向量""" - words = segment_text(text) - vector = {} - for word in words: - vector[word] = vector.get(word, 0) + 1 - return vector - - -def cosine_similarity(v1, v2): - """计算两个向量的余弦相似度""" - dot_product = sum(a * b for a, b in zip(v1, v2)) - norm1 = math.sqrt(sum(a * a for a in v1)) - norm2 = math.sqrt(sum(b * b for b in v2)) - if norm1 == 0 or norm2 == 0: - return 0 - return dot_product / (norm1 * norm2) - - -def visualize_graph_lite(memory_graph: Memory_graph, color_by_memory: bool = False): - # 设置中文字体 - plt.rcParams["font.sans-serif"] = ["SimHei"] # 用来正常显示中文标签 - plt.rcParams["axes.unicode_minus"] = False # 用来正常显示负号 - - G = memory_graph.G - - # 创建一个新图用于可视化 - H = G.copy() - - # 过滤掉内容数量小于2的节点 - nodes_to_remove = [] - for node in H.nodes(): - memory_items = H.nodes[node].get("memory_items", []) - memory_count = len(memory_items) if isinstance(memory_items, list) else (1 if memory_items else 0) - if memory_count < 2: - nodes_to_remove.append(node) - - H.remove_nodes_from(nodes_to_remove) - - # 如果没有符合条件的节点,直接返回 - if len(H.nodes()) == 0: - print("没有找到内容数量大于等于2的节点") - return - - # 计算节点大小和颜色 - node_colors = [] - node_sizes = [] - nodes = list(H.nodes()) - - # 获取最大记忆数用于归一化节点大小 - max_memories = 1 - for node in nodes: - memory_items = H.nodes[node].get("memory_items", []) - memory_count = len(memory_items) if isinstance(memory_items, list) else (1 if memory_items else 0) - max_memories = max(max_memories, memory_count) - - # 计算每个节点的大小和颜色 - for node in nodes: - # 计算节点大小(基于记忆数量) - memory_items = H.nodes[node].get("memory_items", []) - memory_count = len(memory_items) if isinstance(memory_items, list) else (1 if memory_items else 0) - # 使用指数函数使变化更明显 - ratio = memory_count / max_memories - size = 400 + 2000 * (ratio**2) # 增大节点大小 - node_sizes.append(size) - - # 计算节点颜色(基于连接数) - degree = H.degree(node) - if degree >= 30: - node_colors.append((1.0, 0, 0)) # 亮红色 (#FF0000) - else: - # 将1-10映射到0-1的范围 - color_ratio = (degree - 1) / 29.0 if degree > 1 else 0 - # 使用蓝到红的渐变 - red = min(0.9, color_ratio) - blue = max(0.0, 1.0 - color_ratio) - node_colors.append((red, 0, blue)) - - # 绘制图形 - plt.figure(figsize=(16, 12)) # 减小图形尺寸 - pos = nx.spring_layout( - H, - k=1, # 调整节点间斥力 - iterations=100, # 增加迭代次数 - scale=1.5, # 减小布局尺寸 - weight="strength", - ) # 使用边的strength属性作为权重 - - nx.draw( - H, - pos, - with_labels=True, - node_color=node_colors, - node_size=node_sizes, - font_size=12, # 保持增大的字体大小 - font_family="SimHei", - font_weight="bold", - edge_color="gray", - width=1.5, - ) # 统一的边宽度 - - title = """记忆图谱可视化(仅显示内容≥2的节点) -节点大小表示记忆数量 -节点颜色:蓝(弱连接)到红(强连接)渐变,边的透明度表示连接强度 -连接强度越大的节点距离越近""" - plt.title(title, fontsize=16, fontfamily="SimHei") - plt.show() - - -async def main(): - start_time = time.time() - - test_pare = { - "do_build_memory": False, - "do_forget_topic": False, - "do_visualize_graph": True, - "do_query": False, - "do_merge_memory": False, - } - - # 创建记忆图 - memory_graph = Memory_graph() - - # 创建海马体 - hippocampus = Hippocampus(memory_graph) - - # 从数据库同步数据 - hippocampus.sync_memory_from_db() - - end_time = time.time() - logger.info(f"\033[32m[加载海马体耗时: {end_time - start_time:.2f} 秒]\033[0m") - - # 构建记忆 - if test_pare["do_build_memory"]: - logger.info("开始构建记忆...") - chat_size = 20 - await hippocampus.operation_build_memory(chat_size=chat_size) - - end_time = time.time() - logger.info( - f"\033[32m[构建记忆耗时: {end_time - start_time:.2f} 秒,chat_size={chat_size},chat_count = 16]\033[0m" - ) - - if test_pare["do_forget_topic"]: - logger.info("开始遗忘记忆...") - await hippocampus.operation_forget_topic(percentage=0.1) - - end_time = time.time() - logger.info(f"\033[32m[遗忘记忆耗时: {end_time - start_time:.2f} 秒]\033[0m") - - if test_pare["do_merge_memory"]: - logger.info("开始合并记忆...") - await hippocampus.operation_merge_memory(percentage=0.1) - - end_time = time.time() - logger.info(f"\033[32m[合并记忆耗时: {end_time - start_time:.2f} 秒]\033[0m") - - if test_pare["do_visualize_graph"]: - # 展示优化后的图形 - logger.info("生成记忆图谱可视化...") - print("\n生成优化后的记忆图谱:") - visualize_graph_lite(memory_graph) - - if test_pare["do_query"]: - # 交互式查询 - while True: - query = input("\n请输入新的查询概念(输入'退出'以结束):") - if query.lower() == "退出": - break - - items_list = memory_graph.get_related_item(query) - if items_list: - first_layer, second_layer = items_list - if first_layer: - print("\n直接相关的记忆:") - for item in first_layer: - print(f"- {item}") - if second_layer: - print("\n间接相关的记忆:") - for item in second_layer: - print(f"- {item}") - else: - print("未找到相关记忆。") - - -if __name__ == "__main__": - import asyncio - - asyncio.run(main()) diff --git a/src/plugins/memory_system/offline_llm.py b/src/plugins/memory_system/offline_llm.py index e4dc23f93..9c3fa81d9 100644 --- a/src/plugins/memory_system/offline_llm.py +++ b/src/plugins/memory_system/offline_llm.py @@ -10,7 +10,7 @@ from src.common.logger import get_module_logger logger = get_module_logger("offline_llm") -class LLMModel: +class LLM_request_off: def __init__(self, model_name="deepseek-ai/DeepSeek-V3", **kwargs): self.model_name = model_name self.params = kwargs diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py index 5ad69ff25..eed95dd99 100644 --- a/src/plugins/models/utils_model.py +++ b/src/plugins/models/utils_model.py @@ -6,15 +6,13 @@ from typing import Tuple, Union import aiohttp from src.common.logger import get_module_logger -from nonebot import get_driver import base64 from PIL import Image import io from ...common.database import db -from ..chat.config import global_config +from ..config.config import global_config +from ..config.config_env import env_config -driver = get_driver() -config = driver.config logger = get_module_logger("model_utils") @@ -34,8 +32,9 @@ class LLM_request: def __init__(self, model, **kwargs): # 将大写的配置键转换为小写并从config中获取实际值 try: - self.api_key = getattr(config, model["key"]) - self.base_url = getattr(config, model["base_url"]) + self.api_key = getattr(env_config, model["key"]) + self.base_url = getattr(env_config, model["base_url"]) + # print(self.api_key, self.base_url) except AttributeError as e: logger.error(f"原始 model dict 信息:{model}") logger.error(f"配置错误:找不到对应的配置项 - {str(e)}") diff --git a/src/plugins/moods/moods.py b/src/plugins/moods/moods.py index 986075da0..0f3b8deb5 100644 --- a/src/plugins/moods/moods.py +++ b/src/plugins/moods/moods.py @@ -3,7 +3,7 @@ import threading import time from dataclasses import dataclass -from ..chat.config import global_config +from ..config.config import global_config from src.common.logger import get_module_logger, LogConfig, MOOD_STYLE_CONFIG mood_config = LogConfig( diff --git a/src/plugins/remote/remote.py b/src/plugins/remote/remote.py index 8586aa67a..69e18ba79 100644 --- a/src/plugins/remote/remote.py +++ b/src/plugins/remote/remote.py @@ -6,7 +6,7 @@ import os import json import threading from src.common.logger import get_module_logger -from src.plugins.chat.config import global_config +from src.plugins.config.config import global_config logger = get_module_logger("remote") diff --git a/src/plugins/schedule/schedule_generator.py b/src/plugins/schedule/schedule_generator.py index fb79216d5..3d466c887 100644 --- a/src/plugins/schedule/schedule_generator.py +++ b/src/plugins/schedule/schedule_generator.py @@ -10,7 +10,7 @@ sys.path.append(root_path) from src.common.database import db # noqa: E402 from src.common.logger import get_module_logger, SCHEDULE_STYLE_CONFIG, LogConfig # noqa: E402 from src.plugins.models.utils_model import LLM_request # noqa: E402 -from src.plugins.chat.config import global_config # noqa: E402 +from src.plugins.config.config import global_config # noqa: E402 schedule_config = LogConfig( diff --git a/src/plugins/willing/mode_classical.py b/src/plugins/willing/mode_classical.py index 155b2ba71..d9450f028 100644 --- a/src/plugins/willing/mode_classical.py +++ b/src/plugins/willing/mode_classical.py @@ -1,7 +1,7 @@ import asyncio from typing import Dict from ..chat.chat_stream import ChatStream -from ..chat.config import global_config +from ..config.config import global_config class WillingManager: diff --git a/src/plugins/willing/mode_dynamic.py b/src/plugins/willing/mode_dynamic.py index 95942674e..ce188c56c 100644 --- a/src/plugins/willing/mode_dynamic.py +++ b/src/plugins/willing/mode_dynamic.py @@ -3,7 +3,7 @@ import random import time from typing import Dict from src.common.logger import get_module_logger -from ..chat.config import global_config +from ..config.config import global_config from ..chat.chat_stream import ChatStream logger = get_module_logger("mode_dynamic") diff --git a/src/plugins/willing/willing_manager.py b/src/plugins/willing/willing_manager.py index a2f322c1a..ec717d99b 100644 --- a/src/plugins/willing/willing_manager.py +++ b/src/plugins/willing/willing_manager.py @@ -1,7 +1,7 @@ from typing import Optional from src.common.logger import get_module_logger -from ..chat.config import global_config +from ..config.config import global_config from .mode_classical import WillingManager as ClassicalWillingManager from .mode_dynamic import WillingManager as DynamicWillingManager from .mode_custom import WillingManager as CustomWillingManager diff --git a/src/think_flow_demo/current_mind.py b/src/think_flow_demo/current_mind.py index 32d77ef7a..4cb77457d 100644 --- a/src/think_flow_demo/current_mind.py +++ b/src/think_flow_demo/current_mind.py @@ -2,7 +2,7 @@ from .outer_world import outer_world import asyncio from src.plugins.moods.moods import MoodManager from src.plugins.models.utils_model import LLM_request -from src.plugins.chat.config import global_config, BotConfig +from src.plugins.config.config import global_config, BotConfig import re import time from src.plugins.schedule.schedule_generator import bot_schedule diff --git a/src/think_flow_demo/heartflow.py b/src/think_flow_demo/heartflow.py index dcdbe508c..fd60fbb1f 100644 --- a/src/think_flow_demo/heartflow.py +++ b/src/think_flow_demo/heartflow.py @@ -1,7 +1,7 @@ from .current_mind import SubHeartflow from src.plugins.moods.moods import MoodManager from src.plugins.models.utils_model import LLM_request -from src.plugins.chat.config import global_config, BotConfig +from src.plugins.config.config import global_config, BotConfig from src.plugins.schedule.schedule_generator import bot_schedule import asyncio from src.common.logger import get_module_logger, LogConfig, HEARTFLOW_STYLE_CONFIG # noqa: E402 diff --git a/src/think_flow_demo/outer_world.py b/src/think_flow_demo/outer_world.py index c56456bb0..6c32d89de 100644 --- a/src/think_flow_demo/outer_world.py +++ b/src/think_flow_demo/outer_world.py @@ -2,7 +2,7 @@ import asyncio from datetime import datetime from src.plugins.models.utils_model import LLM_request -from src.plugins.chat.config import global_config +from src.plugins.config.config import global_config from src.common.database import db #存储一段聊天的大致内容 From 6128a7f47dbf0239ce29c25c5c03ba37c7bae3b7 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 28 Mar 2025 00:11:32 +0800 Subject: [PATCH 41/46] =?UTF-8?q?better:=E6=B5=B7=E9=A9=AC=E4=BD=932.0?= =?UTF-8?q?=E5=8D=87=E7=BA=A7=EF=BC=8C=E8=BF=9B=E5=BA=A6=2060%=EF=BC=8C?= =?UTF-8?q?=E7=82=B8=E4=BA=86=E5=88=AB=E6=80=AA=E6=88=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/bot.py | 6 +- src/plugins/chat/prompt_builder.py | 9 +- src/plugins/memory_system/Hippocampus.py | 345 +++++++++++++++++----- src/plugins/memory_system/debug_memory.py | 13 +- 4 files changed, 283 insertions(+), 90 deletions(-) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index cc3c43526..5f332ac92 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -129,9 +129,9 @@ class ChatBot: # 根据话题计算激活度 topic = "" - # interested_rate = await HippocampusManager.get_instance().memory_activate_value(message.processed_plain_text) / 100 - interested_rate = 0.1 - logger.debug(f"对{message.processed_plain_text}的激活度:{interested_rate}") + interested_rate = await HippocampusManager.get_instance().get_activate_from_text(message.processed_plain_text) + # interested_rate = 0.1 + logger.info(f"对{message.processed_plain_text}的激活度:{interested_rate}") # logger.info(f"\033[1;32m[主题识别]\033[0m 使用{global_config.topic_extract}主题: {topic}") await self.storage.store_message(message, chat, topic[0] if topic else None) diff --git a/src/plugins/chat/prompt_builder.py b/src/plugins/chat/prompt_builder.py index ea4550329..b75abd6f3 100644 --- a/src/plugins/chat/prompt_builder.py +++ b/src/plugins/chat/prompt_builder.py @@ -80,10 +80,15 @@ class PromptBuilder: # 调用 hippocampus 的 get_relevant_memories 方法 relevant_memories = await HippocampusManager.get_instance().get_memory_from_text( - text=message_txt, num=3, max_depth=2, fast_retrieval=True + text=message_txt, + max_memory_num=4, + max_memory_length=2, + max_depth=3, + fast_retrieval=False ) - # memory_str = "\n".join(memory for topic, memories, _ in relevant_memories for memory in memories) memory_str = "" + for topic, memories in relevant_memories: + memory_str += f"{memories}\n" print(f"memory_str: {memory_str}") if relevant_memories: diff --git a/src/plugins/memory_system/Hippocampus.py b/src/plugins/memory_system/Hippocampus.py index 71956c3f1..edfb0aae3 100644 --- a/src/plugins/memory_system/Hippocampus.py +++ b/src/plugins/memory_system/Hippocampus.py @@ -903,7 +903,7 @@ class Hippocampus: memories.sort(key=lambda x: x[2], reverse=True) return memories - async def get_memory_from_text(self, text: str, num: int = 5, max_depth: int = 3, + async def get_memory_from_text(self, text: str, max_memory_num: int = 3, max_memory_length: int = 2, max_depth: int = 3, fast_retrieval: bool = False) -> list: """从文本中提取关键词并获取相关记忆。 @@ -935,8 +935,8 @@ class Hippocampus: keywords = keywords[:5] else: # 使用LLM提取关键词 - topic_num = min(5, max(1, int(len(text) * 0.2))) # 根据文本长度动态调整关键词数量 - print(f"提取关键词数量: {topic_num}") + topic_num = min(5, max(1, int(len(text) * 0.1))) # 根据文本长度动态调整关键词数量 + # logger.info(f"提取关键词数量: {topic_num}") topics_response = await self.llm_topic_judge.generate_response( self.find_topic_llm(text, topic_num) ) @@ -952,96 +952,276 @@ class Hippocampus: if keyword.strip() ] - logger.info(f"提取的关键词: {', '.join(keywords)}") + # logger.info(f"提取的关键词: {', '.join(keywords)}") + + # 过滤掉不存在于记忆图中的关键词 + valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G] + if not valid_keywords: + logger.info("没有找到有效的关键词节点") + return [] + + logger.info(f"有效的关键词: {', '.join(valid_keywords)}") # 从每个关键词获取记忆 all_memories = [] keyword_connections = [] # 存储关键词之间的连接关系 + activation_words = set(valid_keywords) # 存储所有激活词(包括关键词和途经点) + activate_map = {} # 存储每个词的累计激活值 - # 检查关键词之间的连接 - for i in range(len(keywords)): - for j in range(i + 1, len(keywords)): - keyword1, keyword2 = keywords[i], keywords[j] + # 对每个关键词进行扩散式检索 + for keyword in valid_keywords: + logger.debug(f"开始以关键词 '{keyword}' 为中心进行扩散检索 (最大深度: {max_depth}):") + # 初始化激活值 + activation_values = {keyword: 1.0} + # 记录已访问的节点 + visited_nodes = {keyword} + # 待处理的节点队列,每个元素是(节点, 激活值, 当前深度) + nodes_to_process = [(keyword, 1.0, 0)] + + while nodes_to_process: + current_node, current_activation, current_depth = nodes_to_process.pop(0) - # 检查节点是否存在于图中 - if keyword1 not in self.memory_graph.G or keyword2 not in self.memory_graph.G: - logger.debug(f"关键词 {keyword1} 或 {keyword2} 不在记忆图中") + # 如果激活值小于0或超过最大深度,停止扩散 + if current_activation <= 0 or current_depth >= max_depth: continue + + # 获取当前节点的所有邻居 + neighbors = list(self.memory_graph.G.neighbors(current_node)) - # 检查直接连接 - if self.memory_graph.G.has_edge(keyword1, keyword2): - keyword_connections.append((keyword1, keyword2, 1)) - logger.info(f"发现直接连接: {keyword1} <-> {keyword2} (长度: 1)") - continue - - # 检查间接连接(通过其他节点) - for depth in range(2, max_depth + 1): - # 使用networkx的shortest_path_length检查是否存在指定长度的路径 - try: - path_length = nx.shortest_path_length(self.memory_graph.G, keyword1, keyword2) - if path_length <= depth: - keyword_connections.append((keyword1, keyword2, path_length)) - logger.info(f"发现间接连接: {keyword1} <-> {keyword2} (长度: {path_length})") - # 输出连接路径 - path = nx.shortest_path(self.memory_graph.G, keyword1, keyword2) - logger.info(f"连接路径: {' -> '.join(path)}") - break - except nx.NetworkXNoPath: + for neighbor in neighbors: + if neighbor in visited_nodes: continue + + # 获取连接强度 + edge_data = self.memory_graph.G[current_node][neighbor] + strength = edge_data.get("strength", 1) + + # 计算新的激活值 + new_activation = current_activation - (1 / strength) + + if new_activation > 0: + activation_values[neighbor] = new_activation + visited_nodes.add(neighbor) + nodes_to_process.append((neighbor, new_activation, current_depth + 1)) + logger.debug(f"节点 '{neighbor}' 被激活,激活值: {new_activation:.2f} (通过 '{current_node}' 连接,强度: {strength}, 深度: {current_depth + 1})") + + # 更新激活映射 + for node, activation_value in activation_values.items(): + if activation_value > 0: + if node in activate_map: + activate_map[node] += activation_value + else: + activate_map[node] = activation_value + + # 输出激活映射 + # logger.info("激活映射统计:") + # for node, total_activation in sorted(activate_map.items(), key=lambda x: x[1], reverse=True): + # logger.info(f"节点 '{node}': 累计激活值 = {total_activation:.2f}") - if not keyword_connections: - logger.info("未发现任何关键词之间的连接") + # 基于激活值平方的独立概率选择 + remember_map = {} + logger.info("基于激活值平方的归一化选择:") + + # 计算所有激活值的平方和 + total_squared_activation = sum(activation ** 2 for activation in activate_map.values()) + if total_squared_activation > 0: + # 计算归一化的激活值 + normalized_activations = { + node: (activation ** 2) / total_squared_activation + for node, activation in activate_map.items() + } + + # 按归一化激活值排序并选择前max_memory_num个 + sorted_nodes = sorted( + normalized_activations.items(), + key=lambda x: x[1], + reverse=True + )[:max_memory_num] + + # 将选中的节点添加到remember_map + for node, normalized_activation in sorted_nodes: + remember_map[node] = activate_map[node] # 使用原始激活值 + logger.info(f"节点 '{node}' 被选中 (归一化激活值: {normalized_activation:.2f}, 原始激活值: {activate_map[node]:.2f})") + else: + logger.info("没有有效的激活值") - # 记录已处理的关键词连接 - processed_connections = set() + # 从选中的节点中提取记忆 + all_memories = [] + logger.info("开始从选中的节点中提取记忆:") + for node, activation in remember_map.items(): + logger.debug(f"处理节点 '{node}' (激活值: {activation:.2f}):") + node_data = self.memory_graph.G.nodes[node] + memory_items = node_data.get("memory_items", []) + if not isinstance(memory_items, list): + memory_items = [memory_items] if memory_items else [] + + if memory_items: + logger.debug(f"节点包含 {len(memory_items)} 条记忆") + # 计算每条记忆与输入文本的相似度 + memory_similarities = [] + for memory in memory_items: + # 计算与输入文本的相似度 + memory_words = set(jieba.cut(memory)) + text_words = set(jieba.cut(text)) + all_words = memory_words | text_words + v1 = [1 if word in memory_words else 0 for word in all_words] + v2 = [1 if word in text_words else 0 for word in all_words] + similarity = cosine_similarity(v1, v2) + memory_similarities.append((memory, similarity)) + + # 按相似度排序 + memory_similarities.sort(key=lambda x: x[1], reverse=True) + # 获取最匹配的记忆 + top_memories = memory_similarities[:max_memory_length] + + + # 添加到结果中 + for memory, similarity in top_memories: + all_memories.append((node, [memory], similarity)) + logger.info(f"选中记忆: {memory} (相似度: {similarity:.2f})") + else: + logger.info("节点没有记忆") + + # 去重(基于记忆内容) + logger.debug("开始记忆去重:") + seen_memories = set() + unique_memories = [] + for topic, memory_items, activation_value in all_memories: + memory = memory_items[0] # 因为每个topic只有一条记忆 + if memory not in seen_memories: + seen_memories.add(memory) + unique_memories.append((topic, memory_items, activation_value)) + logger.debug(f"保留记忆: {memory} (来自节点: {topic}, 激活值: {activation_value:.2f})") + else: + logger.debug(f"跳过重复记忆: {memory} (来自节点: {topic})") + + # 转换为(关键词, 记忆)格式 + result = [] + for topic, memory_items, _ in unique_memories: + memory = memory_items[0] # 因为每个topic只有一条记忆 + result.append((topic, memory)) + logger.info(f"选中记忆: {memory} (来自节点: {topic})") + + return result + + async def get_activate_from_text(self, text: str, max_depth: int = 3, + fast_retrieval: bool = False) -> float: + """从文本中提取关键词并获取相关记忆。 + + Args: + text (str): 输入文本 + num (int, optional): 需要返回的记忆数量。默认为5。 + max_depth (int, optional): 记忆检索深度。默认为2。 + fast_retrieval (bool, optional): 是否使用快速检索。默认为False。 + 如果为True,使用jieba分词和TF-IDF提取关键词,速度更快但可能不够准确。 + 如果为False,使用LLM提取关键词,速度较慢但更准确。 + + Returns: + float: 激活节点数与总节点数的比值 + """ + if not text: + return 0 + + if fast_retrieval: + # 使用jieba分词提取关键词 + words = jieba.cut(text) + # 过滤掉停用词和单字词 + keywords = [word for word in words if len(word) > 1] + # 去重 + keywords = list(set(keywords)) + # 限制关键词数量 + keywords = keywords[:5] + else: + # 使用LLM提取关键词 + topic_num = min(5, max(1, int(len(text) * 0.1))) # 根据文本长度动态调整关键词数量 + # logger.info(f"提取关键词数量: {topic_num}") + topics_response = await self.llm_topic_judge.generate_response( + self.find_topic_llm(text, topic_num) + ) + + # 提取关键词 + keywords = re.findall(r'<([^>]+)>', topics_response[0]) + if not keywords: + keywords = ['none'] + else: + keywords = [ + keyword.strip() + for keyword in ','.join(keywords).replace(",", ",").replace("、", ",").replace(" ", ",").split(",") + if keyword.strip() + ] + + # logger.info(f"提取的关键词: {', '.join(keywords)}") + + # 过滤掉不存在于记忆图中的关键词 + valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G] + if not valid_keywords: + logger.info("没有找到有效的关键词节点") + return 0 + + logger.info(f"有效的关键词: {', '.join(valid_keywords)}") # 从每个关键词获取记忆 - for keyword in keywords: - if keyword in self.memory_graph.G: # 只处理存在于图中的关键词 - memories = self.get_memory_from_keyword(keyword, max_depth) - all_memories.extend(memories) + keyword_connections = [] # 存储关键词之间的连接关系 + activation_words = set(valid_keywords) # 存储所有激活词(包括关键词和途经点) + activate_map = {} # 存储每个词的累计激活值 - # 处理关键词连接相关的记忆 - for keyword1, keyword2, path_length in keyword_connections: - if (keyword1, keyword2) in processed_connections or (keyword2, keyword1) in processed_connections: - continue - - processed_connections.add((keyword1, keyword2)) + # 对每个关键词进行扩散式检索 + for keyword in valid_keywords: + logger.debug(f"开始以关键词 '{keyword}' 为中心进行扩散检索 (最大深度: {max_depth}):") + # 初始化激活值 + activation_values = {keyword: 1.0} + # 记录已访问的节点 + visited_nodes = {keyword} + # 待处理的节点队列,每个元素是(节点, 激活值, 当前深度) + nodes_to_process = [(keyword, 1.0, 0)] - # 获取连接路径上的所有节点 - try: - path = nx.shortest_path(self.memory_graph.G, keyword1, keyword2) - for node in path: - if node not in keywords: # 只处理路径上的非关键词节点 - node_data = self.memory_graph.G.nodes[node] - memory_items = node_data.get("memory_items", []) - if not isinstance(memory_items, list): - memory_items = [memory_items] if memory_items else [] + while nodes_to_process: + current_node, current_activation, current_depth = nodes_to_process.pop(0) + + # 如果激活值小于0或超过最大深度,停止扩散 + if current_activation <= 0 or current_depth >= max_depth: + continue + + # 获取当前节点的所有邻居 + neighbors = list(self.memory_graph.G.neighbors(current_node)) + + for neighbor in neighbors: + if neighbor in visited_nodes: + continue - # 计算与输入文本的相似度 - node_words = set(jieba.cut(node)) - text_words = set(jieba.cut(text)) - all_words = node_words | text_words - v1 = [1 if word in node_words else 0 for word in all_words] - v2 = [1 if word in text_words else 0 for word in all_words] - similarity = cosine_similarity(v1, v2) - - if similarity >= 0.3: # 相似度阈值 - all_memories.append((node, memory_items, similarity)) - except nx.NetworkXNoPath: - continue - - # 去重(基于主题) - seen_topics = set() - unique_memories = [] - for topic, memory_items, similarity in all_memories: - if topic not in seen_topics: - seen_topics.add(topic) - unique_memories.append((topic, memory_items, similarity)) - - # 按相似度排序并返回前num个 - unique_memories.sort(key=lambda x: x[2], reverse=True) - return unique_memories[:num] + # 获取连接强度 + edge_data = self.memory_graph.G[current_node][neighbor] + strength = edge_data.get("strength", 1) + + # 计算新的激活值 + new_activation = current_activation - (1 / strength) + + if new_activation > 0: + activation_values[neighbor] = new_activation + visited_nodes.add(neighbor) + nodes_to_process.append((neighbor, new_activation, current_depth + 1)) + logger.debug(f"节点 '{neighbor}' 被激活,激活值: {new_activation:.2f} (通过 '{current_node}' 连接,强度: {strength}, 深度: {current_depth + 1})") + + # 更新激活映射 + for node, activation_value in activation_values.items(): + if activation_value > 0: + if node in activate_map: + activate_map[node] += activation_value + else: + activate_map[node] = activation_value + + # 输出激活映射 + # logger.info("激活映射统计:") + # for node, total_activation in sorted(activate_map.items(), key=lambda x: x[1], reverse=True): + # logger.info(f"节点 '{node}': 累计激活值 = {total_activation:.2f}") + + # 计算激活节点数与总节点数的比值 + total_nodes = len(self.memory_graph.G.nodes()) + activated_nodes = len(activate_map) + activation_ratio = activated_nodes / total_nodes if total_nodes > 0 else 0 + logger.info(f"激活节点数: {activated_nodes}, 总节点数: {total_nodes}, 激活比例: {activation_ratio:.2%}") + + return activation_ratio class HippocampusManager: _instance = None @@ -1109,12 +1289,19 @@ class HippocampusManager: raise RuntimeError("HippocampusManager 尚未初始化,请先调用 initialize 方法") return await self._hippocampus.parahippocampal_gyrus.operation_forget_topic(percentage) - async def get_memory_from_text(self, text: str, num: int = 5, max_depth: int = 2, + async def get_memory_from_text(self, text: str, max_memory_num: int = 3, max_memory_length: int = 2, max_depth: int = 3, fast_retrieval: bool = False) -> list: """从文本中获取相关记忆的公共接口""" if not self._initialized: raise RuntimeError("HippocampusManager 尚未初始化,请先调用 initialize 方法") - return await self._hippocampus.get_memory_from_text(text, num, max_depth, fast_retrieval) + return await self._hippocampus.get_memory_from_text(text, max_memory_num, max_memory_length, max_depth, fast_retrieval) + + async def get_activate_from_text(self, text: str, max_depth: int = 3, + fast_retrieval: bool = False) -> float: + """从文本中获取激活值的公共接口""" + if not self._initialized: + raise RuntimeError("HippocampusManager 尚未初始化,请先调用 initialize 方法") + return await self._hippocampus.get_activate_from_text(text, max_depth, fast_retrieval) def get_memory_from_keyword(self, keyword: str, max_depth: int = 2) -> list: """从关键词获取相关记忆的公共接口""" diff --git a/src/plugins/memory_system/debug_memory.py b/src/plugins/memory_system/debug_memory.py index e24e8c500..4c36767e5 100644 --- a/src/plugins/memory_system/debug_memory.py +++ b/src/plugins/memory_system/debug_memory.py @@ -42,21 +42,22 @@ async def test_memory_system(): [03-24 10:46:49] ❦幻凌慌てない(ta的id:2459587037): 为什么改了回复系数麦麦还是不怎么回复?大佬们''' - test_text = '''千石可乐:niko分不清AI的陪伴和人类的陪伴,是这样吗?''' + # test_text = '''千石可乐:分不清AI的陪伴和人类的陪伴,是这样吗?''' print(f"开始测试记忆检索,测试文本: {test_text}\n") memories = await hippocampus_manager.get_memory_from_text( text=test_text, - num=3, + max_memory_num=3, + max_memory_length=2, max_depth=3, fast_retrieval=False ) + await asyncio.sleep(1) + print("检索到的记忆:") - for topic, memory_items, similarity in memories: + for topic, memory_items in memories: print(f"主题: {topic}") - print(f"相似度: {similarity:.2f}") - for memory in memory_items: - print(f"- {memory}") + print(f"- {memory_items}") From ce254c73bcd4a4155d986859fe0fa678164d20eb Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 28 Mar 2025 00:18:36 +0800 Subject: [PATCH 42/46] =?UTF-8?q?fix:=E4=BF=AE=E6=94=B9=E4=BA=86=E6=84=8F?= =?UTF-8?q?=E6=84=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/bot.py | 2 +- src/plugins/chat/prompt_builder.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 5f332ac92..ac0c352ae 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -129,7 +129,7 @@ class ChatBot: # 根据话题计算激活度 topic = "" - interested_rate = await HippocampusManager.get_instance().get_activate_from_text(message.processed_plain_text) + interested_rate = await HippocampusManager.get_instance().get_activate_from_text(message.processed_plain_text)*20 # interested_rate = 0.1 logger.info(f"对{message.processed_plain_text}的激活度:{interested_rate}") # logger.info(f"\033[1;32m[主题识别]\033[0m 使用{global_config.topic_extract}主题: {topic}") diff --git a/src/plugins/chat/prompt_builder.py b/src/plugins/chat/prompt_builder.py index b75abd6f3..b029ab162 100644 --- a/src/plugins/chat/prompt_builder.py +++ b/src/plugins/chat/prompt_builder.py @@ -84,7 +84,7 @@ class PromptBuilder: max_memory_num=4, max_memory_length=2, max_depth=3, - fast_retrieval=False + fast_retrieval=True ) memory_str = "" for topic, memories in relevant_memories: From e2ae9645efe5298084fa9c153d6d792b6cd2bda8 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 28 Mar 2025 00:40:52 +0800 Subject: [PATCH 43/46] =?UTF-8?q?fix=EF=BC=9A=E5=85=B4=E8=B6=A3=E5=80=BC?= =?UTF-8?q?=E6=9C=89=E5=BE=85=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/bot.py | 2 +- src/plugins/memory_system/Hippocampus.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 4f6159e6b..e974294fa 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -129,7 +129,7 @@ class ChatBot: # 根据话题计算激活度 topic = "" - interested_rate = await HippocampusManager.get_instance().get_activate_from_text(message.processed_plain_text)*20 + interested_rate = await HippocampusManager.get_instance().get_activate_from_text(message.processed_plain_text)*300 # interested_rate = 0.1 logger.info(f"对{message.processed_plain_text}的激活度:{interested_rate}") # logger.info(f"\033[1;32m[主题识别]\033[0m 使用{global_config.topic_extract}主题: {topic}") diff --git a/src/plugins/memory_system/Hippocampus.py b/src/plugins/memory_system/Hippocampus.py index edfb0aae3..f2b3fd3ba 100644 --- a/src/plugins/memory_system/Hippocampus.py +++ b/src/plugins/memory_system/Hippocampus.py @@ -1219,7 +1219,7 @@ class Hippocampus: total_nodes = len(self.memory_graph.G.nodes()) activated_nodes = len(activate_map) activation_ratio = activated_nodes / total_nodes if total_nodes > 0 else 0 - logger.info(f"激活节点数: {activated_nodes}, 总节点数: {total_nodes}, 激活比例: {activation_ratio:.2%}") + logger.info(f"激活节点数: {activated_nodes}, 总节点数: {total_nodes}, 激活比例: {activation_ratio}") return activation_ratio From 4a72fe104a688755bffbe0e1d2640ad29b2ac05c Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 28 Mar 2025 08:06:50 +0800 Subject: [PATCH 44/46] fix:ruff --- src/plugins/chat/bot.py | 3 +- src/plugins/chat/prompt_builder.py | 2 +- src/plugins/chat/utils.py | 1 - src/plugins/memory_system/Hippocampus.py | 41 +++++++++++++---------- src/plugins/memory_system/debug_memory.py | 2 +- 5 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index e974294fa..0c9a5f182 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -129,7 +129,8 @@ class ChatBot: # 根据话题计算激活度 topic = "" - interested_rate = await HippocampusManager.get_instance().get_activate_from_text(message.processed_plain_text)*300 + interested_rate = await HippocampusManager.get_instance().get_activate_from_text( + message.processed_plain_text)*300 # interested_rate = 0.1 logger.info(f"对{message.processed_plain_text}的激活度:{interested_rate}") # logger.info(f"\033[1;32m[主题识别]\033[0m 使用{global_config.topic_extract}主题: {topic}") diff --git a/src/plugins/chat/prompt_builder.py b/src/plugins/chat/prompt_builder.py index b029ab162..521edbcdf 100644 --- a/src/plugins/chat/prompt_builder.py +++ b/src/plugins/chat/prompt_builder.py @@ -87,7 +87,7 @@ class PromptBuilder: fast_retrieval=True ) memory_str = "" - for topic, memories in relevant_memories: + for _topic, memories in relevant_memories: memory_str += f"{memories}\n" print(f"memory_str: {memory_str}") diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index 1b57212a9..163b55301 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -1,4 +1,3 @@ -import math import random import time import re diff --git a/src/plugins/memory_system/Hippocampus.py b/src/plugins/memory_system/Hippocampus.py index f2b3fd3ba..94ffe853d 100644 --- a/src/plugins/memory_system/Hippocampus.py +++ b/src/plugins/memory_system/Hippocampus.py @@ -566,7 +566,8 @@ class ParahippocampalGyrus: logger.debug(input_text) topic_num = self.hippocampus.calculate_topic_num(input_text, compress_rate) - topics_response = await self.hippocampus.llm_topic_judge.generate_response(self.hippocampus.find_topic_llm(input_text, topic_num)) + topics_response = await self.hippocampus.llm_topic_judge.generate_response( + self.hippocampus.find_topic_llm(input_text, topic_num)) # 使用正则表达式提取<>中的内容 topics = re.findall(r'<([^>]+)>', topics_response[0]) @@ -779,16 +780,20 @@ class ParahippocampalGyrus: # 汇总输出所有变化 logger.info("[遗忘] 遗忘操作统计:") if edge_changes["weakened"]: - logger.info(f"[遗忘] 减弱的连接 ({len(edge_changes['weakened'])}个): {', '.join(edge_changes['weakened'])}") + logger.info( + f"[遗忘] 减弱的连接 ({len(edge_changes['weakened'])}个): {', '.join(edge_changes['weakened'])}") if edge_changes["removed"]: - logger.info(f"[遗忘] 移除的连接 ({len(edge_changes['removed'])}个): {', '.join(edge_changes['removed'])}") + logger.info( + f"[遗忘] 移除的连接 ({len(edge_changes['removed'])}个): {', '.join(edge_changes['removed'])}") if node_changes["reduced"]: - logger.info(f"[遗忘] 减少记忆的节点 ({len(node_changes['reduced'])}个): {', '.join(node_changes['reduced'])}") + logger.info( + f"[遗忘] 减少记忆的节点 ({len(node_changes['reduced'])}个): {', '.join(node_changes['reduced'])}") if node_changes["removed"]: - logger.info(f"[遗忘] 移除的节点 ({len(node_changes['removed'])}个): {', '.join(node_changes['removed'])}") + logger.info( + f"[遗忘] 移除的节点 ({len(node_changes['removed'])}个): {', '.join(node_changes['removed'])}") else: logger.info("[遗忘] 本次检查没有节点或连接满足遗忘条件") @@ -903,8 +908,9 @@ class Hippocampus: memories.sort(key=lambda x: x[2], reverse=True) return memories - async def get_memory_from_text(self, text: str, max_memory_num: int = 3, max_memory_length: int = 2, max_depth: int = 3, - fast_retrieval: bool = False) -> list: + async def get_memory_from_text(self, text: str, max_memory_num: int = 3, max_memory_length: int = 2, + max_depth: int = 3, + fast_retrieval: bool = False) -> list: """从文本中提取关键词并获取相关记忆。 Args: @@ -964,8 +970,6 @@ class Hippocampus: # 从每个关键词获取记忆 all_memories = [] - keyword_connections = [] # 存储关键词之间的连接关系 - activation_words = set(valid_keywords) # 存储所有激活词(包括关键词和途经点) activate_map = {} # 存储每个词的累计激活值 # 对每个关键词进行扩散式检索 @@ -1003,7 +1007,8 @@ class Hippocampus: activation_values[neighbor] = new_activation visited_nodes.add(neighbor) nodes_to_process.append((neighbor, new_activation, current_depth + 1)) - logger.debug(f"节点 '{neighbor}' 被激活,激活值: {new_activation:.2f} (通过 '{current_node}' 连接,强度: {strength}, 深度: {current_depth + 1})") + logger.debug( + f"节点 '{neighbor}' 被激活,激活值: {new_activation:.2f} (通过 '{current_node}' 连接,强度: {strength}, 深度: {current_depth + 1})") # noqa: E501 # 更新激活映射 for node, activation_value in activation_values.items(): @@ -1041,7 +1046,8 @@ class Hippocampus: # 将选中的节点添加到remember_map for node, normalized_activation in sorted_nodes: remember_map[node] = activate_map[node] # 使用原始激活值 - logger.info(f"节点 '{node}' 被选中 (归一化激活值: {normalized_activation:.2f}, 原始激活值: {activate_map[node]:.2f})") + logger.info( + f"节点 '{node}' (归一化激活值: {normalized_activation:.2f}, 激活值: {activate_map[node]:.2f})") else: logger.info("没有有效的激活值") @@ -1161,8 +1167,6 @@ class Hippocampus: logger.info(f"有效的关键词: {', '.join(valid_keywords)}") # 从每个关键词获取记忆 - keyword_connections = [] # 存储关键词之间的连接关系 - activation_words = set(valid_keywords) # 存储所有激活词(包括关键词和途经点) activate_map = {} # 存储每个词的累计激活值 # 对每个关键词进行扩散式检索 @@ -1200,7 +1204,8 @@ class Hippocampus: activation_values[neighbor] = new_activation visited_nodes.add(neighbor) nodes_to_process.append((neighbor, new_activation, current_depth + 1)) - logger.debug(f"节点 '{neighbor}' 被激活,激活值: {new_activation:.2f} (通过 '{current_node}' 连接,强度: {strength}, 深度: {current_depth + 1})") + logger.debug( + f"节点 '{neighbor}' 被激活,激活值: {new_activation:.2f} (通过 '{current_node}' 连接,强度: {strength}, 深度: {current_depth + 1})") # noqa: E501 # 更新激活映射 for node, activation_value in activation_values.items(): @@ -1289,12 +1294,14 @@ class HippocampusManager: raise RuntimeError("HippocampusManager 尚未初始化,请先调用 initialize 方法") return await self._hippocampus.parahippocampal_gyrus.operation_forget_topic(percentage) - async def get_memory_from_text(self, text: str, max_memory_num: int = 3, max_memory_length: int = 2, max_depth: int = 3, - fast_retrieval: bool = False) -> list: + async def get_memory_from_text(self, text: str, max_memory_num: int = 3, + max_memory_length: int = 2, max_depth: int = 3, + fast_retrieval: bool = False) -> list: """从文本中获取相关记忆的公共接口""" if not self._initialized: raise RuntimeError("HippocampusManager 尚未初始化,请先调用 initialize 方法") - return await self._hippocampus.get_memory_from_text(text, max_memory_num, max_memory_length, max_depth, fast_retrieval) + return await self._hippocampus.get_memory_from_text( + text, max_memory_num, max_memory_length, max_depth, fast_retrieval) async def get_activate_from_text(self, text: str, max_depth: int = 3, fast_retrieval: bool = False) -> float: diff --git a/src/plugins/memory_system/debug_memory.py b/src/plugins/memory_system/debug_memory.py index 4c36767e5..9baf2e520 100644 --- a/src/plugins/memory_system/debug_memory.py +++ b/src/plugins/memory_system/debug_memory.py @@ -39,7 +39,7 @@ async def test_memory_system(): [03-24 10:46:12] (ta的id:3229291803): [表情包:这张表情包显示了一只手正在做"点赞"的动作,通常表示赞同、喜欢或支持。这个表情包所表达的情感是积极的、赞同的或支持的。] [03-24 10:46:37] 星野風禾(ta的id:2890165435): 还能思考高达 [03-24 10:46:39] 星野風禾(ta的id:2890165435): 什么知识库 -[03-24 10:46:49] ❦幻凌慌てない(ta的id:2459587037): 为什么改了回复系数麦麦还是不怎么回复?大佬们''' +[03-24 10:46:49] ❦幻凌慌てない(ta的id:2459587037): 为什么改了回复系数麦麦还是不怎么回复?大佬们''' # noqa: E501 # test_text = '''千石可乐:分不清AI的陪伴和人类的陪伴,是这样吗?''' From de8d2aba68a0b89c3e7a8bb42685d0d717b28e36 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 28 Mar 2025 09:09:30 +0800 Subject: [PATCH 45/46] =?UTF-8?q?fix=EF=BC=9A=E4=BC=98=E5=8C=96=E6=BF=80?= =?UTF-8?q?=E6=B4=BB=E5=80=BC=EF=BC=8C=E4=BC=98=E5=8C=96logger=E6=98=BE?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/logger.py | 21 +++++++ src/plugins/chat/bot.py | 4 +- src/plugins/chat/prompt_builder.py | 6 +- src/plugins/memory_system/Hippocampus.py | 22 ++++--- src/think_flow_demo/heartflow.py | 4 +- src/think_flow_demo/outer_world.py | 16 ++++- .../{current_mind.py => sub_heartflow.py} | 61 +++++++++++++++---- template/bot_config_template.toml | 2 +- 8 files changed, 102 insertions(+), 34 deletions(-) rename src/think_flow_demo/{current_mind.py => sub_heartflow.py} (74%) diff --git a/src/common/logger.py b/src/common/logger.py index 68de034ed..8556c8058 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -228,6 +228,26 @@ CHAT_STYLE_CONFIG = { }, } +SUB_HEARTFLOW_STYLE_CONFIG = { + "advanced": { + "console_format": ( + "{time:YYYY-MM-DD HH:mm:ss} | " + "{level: <8} | " + "{extra[module]: <12} | " + "麦麦小脑袋 | " + "{message}" + ), + "file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}"), + }, + "simple": { + "console_format": ("{time:MM-DD HH:mm} | 麦麦小脑袋 | {message}"), # noqa: E501 + "file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}"), + }, +} + + + + # 根据SIMPLE_OUTPUT选择配置 MEMORY_STYLE_CONFIG = MEMORY_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else MEMORY_STYLE_CONFIG["advanced"] TOPIC_STYLE_CONFIG = TOPIC_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else TOPIC_STYLE_CONFIG["advanced"] @@ -238,6 +258,7 @@ MOOD_STYLE_CONFIG = MOOD_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else MOOD_STYLE RELATION_STYLE_CONFIG = RELATION_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else RELATION_STYLE_CONFIG["advanced"] SCHEDULE_STYLE_CONFIG = SCHEDULE_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else SCHEDULE_STYLE_CONFIG["advanced"] HEARTFLOW_STYLE_CONFIG = HEARTFLOW_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else HEARTFLOW_STYLE_CONFIG["advanced"] +SUB_HEARTFLOW_STYLE_CONFIG = SUB_HEARTFLOW_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else SUB_HEARTFLOW_STYLE_CONFIG["advanced"] # noqa: E501 def is_registered_module(record: dict) -> bool: """检查是否为已注册的模块""" diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 0c9a5f182..ba8668afa 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -130,9 +130,9 @@ class ChatBot: # 根据话题计算激活度 topic = "" interested_rate = await HippocampusManager.get_instance().get_activate_from_text( - message.processed_plain_text)*300 + message.processed_plain_text,fast_retrieval=True) # interested_rate = 0.1 - logger.info(f"对{message.processed_plain_text}的激活度:{interested_rate}") + # logger.info(f"对{message.processed_plain_text}的激活度:{interested_rate}") # logger.info(f"\033[1;32m[主题识别]\033[0m 使用{global_config.topic_extract}主题: {topic}") await self.storage.store_message(message, chat, topic[0] if topic else None) diff --git a/src/plugins/chat/prompt_builder.py b/src/plugins/chat/prompt_builder.py index 521edbcdf..c527df647 100644 --- a/src/plugins/chat/prompt_builder.py +++ b/src/plugins/chat/prompt_builder.py @@ -81,15 +81,15 @@ class PromptBuilder: # 调用 hippocampus 的 get_relevant_memories 方法 relevant_memories = await HippocampusManager.get_instance().get_memory_from_text( text=message_txt, - max_memory_num=4, + max_memory_num=3, max_memory_length=2, max_depth=3, - fast_retrieval=True + fast_retrieval=False ) memory_str = "" for _topic, memories in relevant_memories: memory_str += f"{memories}\n" - print(f"memory_str: {memory_str}") + # print(f"memory_str: {memory_str}") if relevant_memories: # 格式化记忆内容 diff --git a/src/plugins/memory_system/Hippocampus.py b/src/plugins/memory_system/Hippocampus.py index 94ffe853d..7cd8ff744 100644 --- a/src/plugins/memory_system/Hippocampus.py +++ b/src/plugins/memory_system/Hippocampus.py @@ -817,8 +817,8 @@ class Hippocampus: self.parahippocampal_gyrus = ParahippocampalGyrus(self) # 从数据库加载记忆图 self.entorhinal_cortex.sync_memory_from_db() - self.llm_topic_judge = LLM_request(self.config.llm_topic_judge) - self.llm_summary_by_topic = LLM_request(self.config.llm_summary_by_topic) + self.llm_topic_judge = LLM_request(self.config.llm_topic_judge,request_type="memory") + self.llm_summary_by_topic = LLM_request(self.config.llm_summary_by_topic,request_type="memory") def get_all_node_names(self) -> list: """获取记忆图中所有节点的名字列表""" @@ -950,7 +950,7 @@ class Hippocampus: # 提取关键词 keywords = re.findall(r'<([^>]+)>', topics_response[0]) if not keywords: - keywords = ['none'] + keywords = [] else: keywords = [ keyword.strip() @@ -1025,7 +1025,7 @@ class Hippocampus: # 基于激活值平方的独立概率选择 remember_map = {} - logger.info("基于激活值平方的归一化选择:") + # logger.info("基于激活值平方的归一化选择:") # 计算所有激活值的平方和 total_squared_activation = sum(activation ** 2 for activation in activate_map.values()) @@ -1079,12 +1079,11 @@ class Hippocampus: memory_similarities.sort(key=lambda x: x[1], reverse=True) # 获取最匹配的记忆 top_memories = memory_similarities[:max_memory_length] - # 添加到结果中 for memory, similarity in top_memories: all_memories.append((node, [memory], similarity)) - logger.info(f"选中记忆: {memory} (相似度: {similarity:.2f})") + # logger.info(f"选中记忆: {memory} (相似度: {similarity:.2f})") else: logger.info("节点没有记忆") @@ -1148,7 +1147,7 @@ class Hippocampus: # 提取关键词 keywords = re.findall(r'<([^>]+)>', topics_response[0]) if not keywords: - keywords = ['none'] + keywords = [] else: keywords = [ keyword.strip() @@ -1221,10 +1220,13 @@ class Hippocampus: # logger.info(f"节点 '{node}': 累计激活值 = {total_activation:.2f}") # 计算激活节点数与总节点数的比值 + total_activation = sum(activate_map.values()) + logger.info(f"总激活值: {total_activation:.2f}") total_nodes = len(self.memory_graph.G.nodes()) - activated_nodes = len(activate_map) - activation_ratio = activated_nodes / total_nodes if total_nodes > 0 else 0 - logger.info(f"激活节点数: {activated_nodes}, 总节点数: {total_nodes}, 激活比例: {activation_ratio}") + # activated_nodes = len(activate_map) + activation_ratio = total_activation / total_nodes if total_nodes > 0 else 0 + activation_ratio = activation_ratio*40 + logger.info(f"总激活值: {total_activation:.2f}, 总节点数: {total_nodes}, 激活: {activation_ratio}") return activation_ratio diff --git a/src/think_flow_demo/heartflow.py b/src/think_flow_demo/heartflow.py index fd60fbb1f..724ccfda3 100644 --- a/src/think_flow_demo/heartflow.py +++ b/src/think_flow_demo/heartflow.py @@ -1,4 +1,4 @@ -from .current_mind import SubHeartflow +from .sub_heartflow import SubHeartflow from src.plugins.moods.moods import MoodManager from src.plugins.models.utils_model import LLM_request from src.plugins.config.config import global_config, BotConfig @@ -46,7 +46,7 @@ class Heartflow: logger.info("麦麦大脑袋转起来了") self.current_state.update_current_state_info() - personality_info = " ".join(BotConfig.PROMPT_PERSONALITY) + personality_info = " ".join(global_config.PROMPT_PERSONALITY) current_thinking_info = self.current_mind mood_info = self.current_state.mood related_memory_info = 'memory' diff --git a/src/think_flow_demo/outer_world.py b/src/think_flow_demo/outer_world.py index 6c32d89de..fb44241dc 100644 --- a/src/think_flow_demo/outer_world.py +++ b/src/think_flow_demo/outer_world.py @@ -16,6 +16,10 @@ class Talking_info: self.observe_times = 0 self.activate = 360 + self.last_summary_time = int(datetime.now().timestamp()) # 上次更新summary的时间 + self.summary_count = 0 # 30秒内的更新次数 + self.max_update_in_30s = 2 + self.oberve_interval = 3 self.llm_summary = LLM_request( @@ -60,16 +64,22 @@ class Talking_info: if len(self.talking_message) > 20: self.talking_message = self.talking_message[-20:] # 只保留最新的20条 self.translate_message_list_to_str() - # print(self.talking_message_str) self.observe_times += 1 self.last_observe_time = new_messages[-1]["time"] - if self.observe_times > 3: + # 检查是否需要更新summary + current_time = int(datetime.now().timestamp()) + if current_time - self.last_summary_time >= 30: # 如果超过30秒,重置计数 + self.summary_count = 0 + self.last_summary_time = current_time + + if self.summary_count < self.max_update_in_30s: # 如果30秒内更新次数小于2次 await self.update_talking_summary() - # print(f"更新了聊天总结:{self.talking_summary}") + self.summary_count += 1 async def update_talking_summary(self): #基于已经有的talking_summary,和新的talking_message,生成一个summary + # print(f"更新聊天总结:{self.talking_summary}") prompt = "" prompt = f"你正在参与一个qq群聊的讨论,这个群之前在聊的内容是:{self.talking_summary}\n" prompt += f"现在群里的群友们产生了新的讨论,有了新的发言,具体内容如下:{self.talking_message_str}\n" diff --git a/src/think_flow_demo/current_mind.py b/src/think_flow_demo/sub_heartflow.py similarity index 74% rename from src/think_flow_demo/current_mind.py rename to src/think_flow_demo/sub_heartflow.py index 4cb77457d..b2179dc43 100644 --- a/src/think_flow_demo/current_mind.py +++ b/src/think_flow_demo/sub_heartflow.py @@ -6,6 +6,16 @@ from src.plugins.config.config import global_config, BotConfig import re import time from src.plugins.schedule.schedule_generator import bot_schedule +from src.plugins.memory_system.Hippocampus import HippocampusManager +from src.common.logger import get_module_logger, LogConfig, SUB_HEARTFLOW_STYLE_CONFIG # noqa: E402 + +subheartflow_config = LogConfig( + # 使用海马体专用样式 + console_format=SUB_HEARTFLOW_STYLE_CONFIG["console_format"], + file_format=SUB_HEARTFLOW_STYLE_CONFIG["file_format"], +) +logger = get_module_logger("subheartflow", config=subheartflow_config) + class CuttentState: def __init__(self): @@ -37,7 +47,7 @@ class SubHeartflow: if not self.current_mind: self.current_mind = "你什么也没想" - self.personality_info = " ".join(BotConfig.PROMPT_PERSONALITY) + self.personality_info = " ".join(global_config.PROMPT_PERSONALITY) def assign_observe(self,stream_id): self.outer_world = outer_world.get_world_by_stream_id(stream_id) @@ -55,23 +65,42 @@ class SubHeartflow: await asyncio.sleep(60) async def do_a_thinking(self): - print("麦麦小脑袋转起来了") self.current_state.update_current_state_info() current_thinking_info = self.current_mind mood_info = self.current_state.mood - related_memory_info = '' + message_stream_info = self.outer_world.talking_summary - schedule_info = bot_schedule.get_current_num_task(num = 2,time_info = False) + print(f"message_stream_info:{message_stream_info}") + + related_memory = await HippocampusManager.get_instance().get_memory_from_text( + text=message_stream_info, + max_memory_num=3, + max_memory_length=2, + max_depth=3, + fast_retrieval=False + ) + # print(f"相关记忆:{related_memory}") + if related_memory: + related_memory_info = "" + for memory in related_memory: + related_memory_info += memory[1] + else: + related_memory_info = '' + + print(f"相关记忆:{related_memory_info}") + + schedule_info = bot_schedule.get_current_num_task(num = 1,time_info = False) prompt = "" prompt += f"你刚刚在做的事情是:{schedule_info}\n" # prompt += f"麦麦的总体想法是:{self.main_heartflow_info}\n\n" - prompt += f"{self.personality_info}\n" + prompt += f"你{self.personality_info}\n" prompt += f"现在你正在上网,和qq群里的网友们聊天,群里正在聊的话题是:{message_stream_info}\n" - prompt += f"你想起来{related_memory_info}。" + if related_memory_info: + prompt += f"你想起来{related_memory_info}。" prompt += f"刚刚你的想法是{current_thinking_info}。" - prompt += f"你现在{mood_info}。" + prompt += f"你现在{mood_info}。\n" prompt += "现在你接下去继续思考,产生新的想法,不要分点输出,输出连贯的内心独白,不要太长," prompt += "但是记得结合上述的消息,要记得维持住你的人设,关注聊天和新内容,不要思考太多:" reponse, reasoning_content = await self.llm_model.generate_response_async(prompt) @@ -79,7 +108,8 @@ class SubHeartflow: self.update_current_mind(reponse) self.current_mind = reponse - print(f"麦麦的脑内状态:{self.current_mind}") + print(prompt) + logger.info(f"麦麦的脑内状态:{self.current_mind}") async def do_after_reply(self,reply_content,chat_talking_prompt): # print("麦麦脑袋转起来了") @@ -91,24 +121,29 @@ class SubHeartflow: message_stream_info = self.outer_world.talking_summary message_new_info = chat_talking_prompt reply_info = reply_content + schedule_info = bot_schedule.get_current_num_task(num = 1,time_info = False) + prompt = "" - prompt += f"{self.personality_info}\n" + prompt += f"你刚刚在做的事情是:{schedule_info}\n" + prompt += f"你{self.personality_info}\n" + prompt += f"现在你正在上网,和qq群里的网友们聊天,群里正在聊的话题是:{message_stream_info}\n" - prompt += f"你想起来{related_memory_info}。" + if related_memory_info: + prompt += f"你想起来{related_memory_info}。" prompt += f"刚刚你的想法是{current_thinking_info}。" prompt += f"你现在看到了网友们发的新消息:{message_new_info}\n" prompt += f"你刚刚回复了群友们:{reply_info}" prompt += f"你现在{mood_info}。" prompt += "现在你接下去继续思考,产生新的想法,记得保留你刚刚的想法,不要分点输出,输出连贯的内心独白" - prompt += "不要太长,但是记得结合上述的消息,要记得你的人设,关注聊天和新内容,以及你回复的内容,不要思考太多:" + prompt += "不要太长,但是记得结合上述的消息,要记得你的人设,关注聊天和新内容,关注你回复的内容,不要思考太多:" reponse, reasoning_content = await self.llm_model.generate_response_async(prompt) self.update_current_mind(reponse) self.current_mind = reponse - print(f"{self.observe_chat_id}麦麦的脑内状态:{self.current_mind}") + logger.info(f"麦麦回复后的脑内状态:{self.current_mind}") self.last_reply_time = time.time() @@ -133,7 +168,7 @@ class SubHeartflow: else: self.current_state.willing = 0 - print(f"{self.observe_chat_id}麦麦的回复意愿:{self.current_state.willing}") + logger.info(f"{self.observe_chat_id}麦麦的回复意愿:{self.current_state.willing}") return self.current_state.willing diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index b64e79f2c..acec0697e 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -3,7 +3,7 @@ version = "0.0.11" [mai_version] version = "0.6.0" -version-fix = "snapshot-1" +version-fix = "snapshot-2" #以下是给开发人员阅读的,一般用户不需要阅读 #如果你想要修改配置文件,请在修改后将version的值进行变更 From 94a554699e0be0641ae918df71ca1e480a682339 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 28 Mar 2025 09:34:21 +0800 Subject: [PATCH 46/46] =?UTF-8?q?better=EF=BC=9A=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E5=92=8C=E5=BF=83=E6=B5=81=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/logger.py | 4 ++-- src/plugins/chat/prompt_builder.py | 2 +- src/plugins/memory_system/Hippocampus.py | 4 ++-- src/plugins/utils/statistic.py | 14 ++++++++++---- src/think_flow_demo/heartflow.py | 7 ++++--- src/think_flow_demo/sub_heartflow.py | 18 ++++++++++-------- 6 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/common/logger.py b/src/common/logger.py index 8556c8058..ef41f87ab 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -81,7 +81,7 @@ MEMORY_STYLE_CONFIG = { "file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 海马体 | {message}"), }, "simple": { - "console_format": ("{time:MM-DD HH:mm} | 海马体 | {message}"), + "console_format": ("{time:MM-DD HH:mm} | 海马体 | {message}"), "file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 海马体 | {message}"), }, } @@ -240,7 +240,7 @@ SUB_HEARTFLOW_STYLE_CONFIG = { "file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}"), }, "simple": { - "console_format": ("{time:MM-DD HH:mm} | 麦麦小脑袋 | {message}"), # noqa: E501 + "console_format": ("{time:MM-DD HH:mm} | 麦麦小脑袋 | {message}"), # noqa: E501 "file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}"), }, } diff --git a/src/plugins/chat/prompt_builder.py b/src/plugins/chat/prompt_builder.py index c527df647..6b33e9881 100644 --- a/src/plugins/chat/prompt_builder.py +++ b/src/plugins/chat/prompt_builder.py @@ -83,7 +83,7 @@ class PromptBuilder: text=message_txt, max_memory_num=3, max_memory_length=2, - max_depth=3, + max_depth=4, fast_retrieval=False ) memory_str = "" diff --git a/src/plugins/memory_system/Hippocampus.py b/src/plugins/memory_system/Hippocampus.py index 7cd8ff744..bdb2a50b1 100644 --- a/src/plugins/memory_system/Hippocampus.py +++ b/src/plugins/memory_system/Hippocampus.py @@ -1046,14 +1046,14 @@ class Hippocampus: # 将选中的节点添加到remember_map for node, normalized_activation in sorted_nodes: remember_map[node] = activate_map[node] # 使用原始激活值 - logger.info( + logger.debug( f"节点 '{node}' (归一化激活值: {normalized_activation:.2f}, 激活值: {activate_map[node]:.2f})") else: logger.info("没有有效的激活值") # 从选中的节点中提取记忆 all_memories = [] - logger.info("开始从选中的节点中提取记忆:") + # logger.info("开始从选中的节点中提取记忆:") for node, activation in remember_map.items(): logger.debug(f"处理节点 '{node}' (激活值: {activation:.2f}):") node_data = self.memory_graph.G.nodes[node] diff --git a/src/plugins/utils/statistic.py b/src/plugins/utils/statistic.py index aad33e88c..1071b29b0 100644 --- a/src/plugins/utils/statistic.py +++ b/src/plugins/utils/statistic.py @@ -44,13 +44,19 @@ class LLMStatistics: def _record_online_time(self): """记录在线时间""" - try: + current_time = datetime.now() + # 检查5分钟内是否已有记录 + recent_record = db.online_time.find_one({ + "timestamp": { + "$gte": current_time - timedelta(minutes=5) + } + }) + + if not recent_record: db.online_time.insert_one({ - "timestamp": datetime.now(), + "timestamp": current_time, "duration": 5 # 5分钟 }) - except Exception: - logger.exception("记录在线时间失败") def _collect_statistics_for_period(self, start_time: datetime) -> Dict[str, Any]: """收集指定时间段的LLM请求统计数据 diff --git a/src/think_flow_demo/heartflow.py b/src/think_flow_demo/heartflow.py index 724ccfda3..45bf3a852 100644 --- a/src/think_flow_demo/heartflow.py +++ b/src/think_flow_demo/heartflow.py @@ -35,6 +35,7 @@ class Heartflow: self._subheartflows = {} self.active_subheartflows_nums = 0 + self.personality_info = " ".join(global_config.PROMPT_PERSONALITY) async def heartflow_start_working(self): @@ -46,13 +47,13 @@ class Heartflow: logger.info("麦麦大脑袋转起来了") self.current_state.update_current_state_info() - personality_info = " ".join(global_config.PROMPT_PERSONALITY) + personality_info = self.personality_info current_thinking_info = self.current_mind mood_info = self.current_state.mood related_memory_info = 'memory' sub_flows_info = await self.get_all_subheartflows_minds() - schedule_info = bot_schedule.get_current_num_task(num = 5,time_info = True) + schedule_info = bot_schedule.get_current_num_task(num = 4,time_info = True) prompt = "" prompt += f"你刚刚在做的事情是:{schedule_info}\n" @@ -91,7 +92,7 @@ class Heartflow: return await self.minds_summary(sub_minds) async def minds_summary(self,minds_str): - personality_info = " ".join(BotConfig.PROMPT_PERSONALITY) + personality_info = self.personality_info mood_info = self.current_state.mood prompt = "" diff --git a/src/think_flow_demo/sub_heartflow.py b/src/think_flow_demo/sub_heartflow.py index b2179dc43..805218d5a 100644 --- a/src/think_flow_demo/sub_heartflow.py +++ b/src/think_flow_demo/sub_heartflow.py @@ -75,7 +75,7 @@ class SubHeartflow: related_memory = await HippocampusManager.get_instance().get_memory_from_text( text=message_stream_info, - max_memory_num=3, + max_memory_num=2, max_memory_length=2, max_depth=3, fast_retrieval=False @@ -96,10 +96,12 @@ class SubHeartflow: prompt += f"你刚刚在做的事情是:{schedule_info}\n" # prompt += f"麦麦的总体想法是:{self.main_heartflow_info}\n\n" prompt += f"你{self.personality_info}\n" - prompt += f"现在你正在上网,和qq群里的网友们聊天,群里正在聊的话题是:{message_stream_info}\n" if related_memory_info: - prompt += f"你想起来{related_memory_info}。" - prompt += f"刚刚你的想法是{current_thinking_info}。" + prompt += f"你想起来你之前见过的回忆:{related_memory_info}。\n以上是你的回忆,不一定是目前聊天里的人说的,也不一定是现在发生的事情,请记住。\n" + prompt += f"刚刚你的想法是{current_thinking_info}。\n" + prompt += "-----------------------------------\n" + if message_stream_info: + prompt += f"现在你正在上网,和qq群里的网友们聊天,群里正在聊的话题是:{message_stream_info}\n" prompt += f"你现在{mood_info}。\n" prompt += "现在你接下去继续思考,产生新的想法,不要分点输出,输出连贯的内心独白,不要太长," prompt += "但是记得结合上述的消息,要记得维持住你的人设,关注聊天和新内容,不要思考太多:" @@ -108,7 +110,7 @@ class SubHeartflow: self.update_current_mind(reponse) self.current_mind = reponse - print(prompt) + logger.info(f"prompt:\n{prompt}\n") logger.info(f"麦麦的脑内状态:{self.current_mind}") async def do_after_reply(self,reply_content,chat_talking_prompt): @@ -117,7 +119,7 @@ class SubHeartflow: current_thinking_info = self.current_mind mood_info = self.current_state.mood - related_memory_info = 'memory' + # related_memory_info = 'memory' message_stream_info = self.outer_world.talking_summary message_new_info = chat_talking_prompt reply_info = reply_content @@ -129,8 +131,8 @@ class SubHeartflow: prompt += f"你{self.personality_info}\n" prompt += f"现在你正在上网,和qq群里的网友们聊天,群里正在聊的话题是:{message_stream_info}\n" - if related_memory_info: - prompt += f"你想起来{related_memory_info}。" + # if related_memory_info: + # prompt += f"你想起来{related_memory_info}。" prompt += f"刚刚你的想法是{current_thinking_info}。" prompt += f"你现在看到了网友们发的新消息:{message_new_info}\n" prompt += f"你刚刚回复了群友们:{reply_info}"