From a4105d06924f7547f00e85eb59ca2136c3b48fe9 Mon Sep 17 00:00:00 2001 From: Ronifue Date: Tue, 15 Apr 2025 16:24:26 +0800 Subject: [PATCH 01/28] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E3=80=90?= =?UTF-8?q?=E6=9B=B4=E5=A5=BD=E7=9A=84=E8=A1=A8=E6=83=85=E5=8C=85=E5=8F=91?= =?UTF-8?q?=E9=80=81=E7=B3=BB=E7=BB=9F=E3=80=91=EF=BC=88=E8=BF=AB=E7=9C=9F?= =?UTF-8?q?=EF=BC=89=E5=AF=BC=E8=87=B4=E7=9A=84=E9=A2=9C=E6=96=87=E5=AD=97?= =?UTF-8?q?=E6=B6=88=E5=A4=B1=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/utils.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index b4e2cb3c2..ba4781acc 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -324,11 +324,16 @@ def random_remove_punctuation(text: str) -> str: def process_llm_response(text: str) -> List[str]: + # 先保护颜文字 + protected_text, kaomoji_mapping = protect_kaomoji(text) + logger.debug(f"保护颜文字后的文本: {protected_text}") # 提取被 () 或 [] 包裹的内容 pattern = re.compile(r"[\(\[].*?[\)\]]") - _extracted_contents = pattern.findall(text) + # _extracted_contents = pattern.findall(text) + _extracted_contents = pattern.findall(protected_text) # 在保护后的文本上查找 # 去除 () 和 [] 及其包裹的内容 - cleaned_text = pattern.sub("", text) + # cleaned_text = pattern.sub("", text) + cleaned_text = pattern.sub("", protected_text) logger.debug(f"{text}去除括号处理后的文本: {cleaned_text}") # 对清理后的文本进行进一步处理 @@ -368,6 +373,8 @@ def process_llm_response(text: str) -> List[str]: return [f"{global_config.BOT_NICKNAME}不知道哦"] # sentences.extend(extracted_contents) + # 在所有句子处理完毕后,对包含占位符的列表进行恢复 + sentences = recover_kaomoji(sentences, kaomoji_mapping) return sentences From 071366f89ce7ed2e9fb16e4c308ea0b2d87c1a63 Mon Sep 17 00:00:00 2001 From: HexatomicRing <54496918+HexatomicRing@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:33:39 +0800 Subject: [PATCH 02/28] =?UTF-8?q?=E4=BD=BF=E7=94=A8=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E6=97=B6=E8=80=83=E8=99=91=E4=B8=8A=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/do_tool/tool_use.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/do_tool/tool_use.py b/src/do_tool/tool_use.py index b14927be8..2bb8a2c65 100644 --- a/src/do_tool/tool_use.py +++ b/src/do_tool/tool_use.py @@ -1,3 +1,4 @@ +from src.plugins.chat.utils import get_recent_group_detailed_plain_text from src.plugins.models.utils_model import LLM_request from src.plugins.config.config import global_config from src.plugins.chat.chat_stream import ChatStream @@ -41,6 +42,12 @@ class ToolUser: else: mid_memory_info = "" + stream_id = 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 + ) new_messages = list( db.messages.find({"chat_id": chat_stream.stream_id, "time": {"$gt": time.time()}}).sort("time", 1).limit(15) ) @@ -54,9 +61,10 @@ class ToolUser: prompt = "" prompt += mid_memory_info prompt += "你正在思考如何回复群里的消息。\n" + prompt += f"之前群里进行了如下讨论:\n" + prompt += chat_talking_prompt prompt += f"你注意到{sender_name}刚刚说:{message_txt}\n" - prompt += f"注意你就是{bot_name},{bot_name}指的就是你。" - + prompt += f"注意你就是{bot_name},{bot_name}是你的名字。根据之前的聊天记录补充问题信息,搜索时避开你的名字。\n" prompt += "你现在需要对群里的聊天内容进行回复,现在选择工具来对消息和你的回复进行处理,你是否需要额外的信息,比如回忆或者搜寻已有的知识,改变关系和情感,或者了解你现在正在做什么。" return prompt From a0e4d5ea385b192b64f755aff02d62fbca076393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Thu, 10 Apr 2025 22:31:22 +0800 Subject: [PATCH 03/28] =?UTF-8?q?=E6=9B=B4=E6=96=B0README=EF=BC=8C?= =?UTF-8?q?=E5=92=8CDev=E5=88=86=E6=94=AF=E4=BF=9D=E6=8C=81=E4=B8=80?= =?UTF-8?q?=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 325e3ad22..beea5757f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@
- ![Python Version](https://img.shields.io/badge/Python-3.10+-blue) + ![Python Version](https://img.shields.io/badge/Python-3.9+-blue) ![License](https://img.shields.io/github/license/SengokuCola/MaiMBot?label=协议) ![Status](https://img.shields.io/badge/状态-开发中-yellow) ![Contributors](https://img.shields.io/github/contributors/MaiM-with-u/MaiBot.svg?style=flat&label=贡献者) @@ -37,7 +37,7 @@

-## 新版0.6.x部署前先阅读:https://docs.mai-mai.org/manual/usage/mmc_q_a +## 新版0.6.0部署前先阅读:https://docs.mai-mai.org/manual/usage/mmc_q_a ## 📝 项目简介 @@ -60,6 +60,23 @@
+### 📢 版本信息 + +- 💭 **智能对话系统**:基于LLM的自然语言交互 +- 🤔 **实时思维系统**:模拟人类思考过程 +- 💝 **情感表达系统**:丰富的表情包和情绪表达 +- 🧠 **持久记忆系统**:基于MongoDB的长期记忆存储 +- 🔄 **动态人格系统**:自适应的性格特征 + +
+ + 麦麦演示视频 +
+ 👆 点击观看麦麦演示视频 👆 +
+
+ + ### 📢 版本信息 **最新版本: v0.6.2** ([查看更新日志](changelogs/changelog.md)) @@ -86,7 +103,7 @@ ### ⚠️ 重要提示 -- 升级到v0.6.x版本前请务必阅读:[升级指南](https://docs.mai-mai.org/manual/usage/mmc_q_a) +- 升级到v0.6.0版本前请务必阅读:[升级指南](https://docs.mai-mai.org/manual/usage/mmc_q_a) - 本版本基于MaiCore重构,通过nonebot插件与QQ平台交互 - 项目处于活跃开发阶段,功能和API可能随时调整 @@ -115,22 +132,21 @@ | 模块 | 主要功能 | 特点 | |------|---------|------| -| 💬 聊天系统 | • 心流/推理聊天
• 关键词主动发言
• 多模型支持
• 动态prompt构建
• 私聊功能(PFC) | 拟人化交互 | -| 🧠 心流系统 | • 实时思考生成
• 自动启停机制
• 日程系统联动
• 工具调用能力 | 智能化决策 | -| 🧠 记忆系统 | • 优化记忆抽取
• 海马体记忆机制
• 聊天记录概括 | 持久化记忆 | -| 😊 表情系统 | • 情绪匹配发送
• GIF支持
• 自动收集与审查 | 丰富表达 | +| 💬 聊天系统 | • 思维流/推理聊天
• 关键词主动发言
• 多模型支持
• 动态prompt构建
• 私聊功能(PFC) | 拟人化交互 | +| 🧠 思维流系统 | • 实时思考生成
• 自动启停机制
• 日程系统联动 | 智能化决策 | +| 🧠 记忆系统 2.0 | • 优化记忆抽取
• 海马体记忆机制
• 聊天记录概括 | 持久化记忆 | +| 😊 表情包系统 | • 情绪匹配发送
• GIF支持
• 自动收集与审查 | 丰富表达 | | 📅 日程系统 | • 动态日程生成
• 自定义想象力
• 思维流联动 | 智能规划 | -| 👥 关系系统 | • 关系管理优化
• 丰富接口支持
• 个性化交互 | 深度社交 | +| 👥 关系系统 2.0 | • 关系管理优化
• 丰富接口支持
• 个性化交互 | 深度社交 | | 📊 统计系统 | • 使用数据统计
• LLM调用记录
• 实时控制台显示 | 数据可视 | | 🔧 系统功能 | • 优雅关闭机制
• 自动数据保存
• 异常处理完善 | 稳定可靠 | -| 🛠️ 工具系统 | • 知识获取工具
• 自动注册机制
• 多工具支持 | 扩展功能 | ## 📐 项目架构 ```mermaid graph TD A[MaiCore] --> B[对话系统] - A --> C[心流系统] + A --> C[思维流系统] A --> D[记忆系统] A --> E[情感系统] B --> F[多模型支持] From 3a04e8a6ba295f33f8459760681cb36cf044087d Mon Sep 17 00:00:00 2001 From: Ark-Hakobune Date: Tue, 15 Apr 2025 21:58:20 +0800 Subject: [PATCH 04/28] =?UTF-8?q?=E7=A7=BB=E9=99=A4=20maim=5Fmessage=20?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E8=B7=9F=E8=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/willing/mode_llmcheck.py | 164 +++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 src/plugins/willing/mode_llmcheck.py diff --git a/src/plugins/willing/mode_llmcheck.py b/src/plugins/willing/mode_llmcheck.py new file mode 100644 index 000000000..62a8cedba --- /dev/null +++ b/src/plugins/willing/mode_llmcheck.py @@ -0,0 +1,164 @@ +import time +from loguru import logger +from ..schedule.schedule_generator import bot_schedule +from ..models.utils_model import LLM_request + +from ..config.config import global_config +from ..chat.chat_stream import ChatStream +from .mode_classical import WillingManager +from ..chat.utils import get_recent_group_detailed_plain_text + + +import re +from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig + +# 定义日志配置 +chat_config = LogConfig( + # 使用消息发送专用样式 + console_format=CHAT_STYLE_CONFIG["console_format"], + file_format=CHAT_STYLE_CONFIG["file_format"], +) + +# 配置主程序日志格式 +logger = get_module_logger("llm_willing", config=chat_config) + +class WillingManager_llm(WillingManager): + + def __init__(self): + super().__init__() + self.model_v3 = LLM_request(model=global_config.llm_normal, temperature=0.3) + + 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, + **kwargs) -> float: + stream_id = chat_stream.stream_id + if chat_stream.group_info and config: + if chat_stream.group_info.group_id not in config.talk_allowed_groups: + reply_probability = 0 + return reply_probability + + 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 + chat_talking_prompt = "" + if stream_id: + chat_talking_prompt = get_recent_group_detailed_plain_text( + stream_id, limit=5, combine=True + ) + if chat_stream.group_info: + if str(config.BOT_QQ) in chat_talking_prompt: + pass + # logger.info(f"{chat_talking_prompt}") + # logger.info(f"bot在群聊中5条内发过言,启动llm计算回复概率") + else: + return self.default_change_reply_willing_received( + chat_stream=chat_stream, + is_mentioned_bot=is_mentioned_bot, + config=config, + is_emoji=is_emoji, + interested_rate=interested_rate, + sender_id=sender_id, + ) + else: + chat_in_group = False + chat_talking_prompt = chat_talking_prompt + # print(f"\033[1;34m[调试]\033[0m 已从数据库获取群 {group_id} 的消息记录:{chat_talking_prompt}") + + # if is_mentioned_bot: + # return 1.0 + prompt = f""" + 假设你正在查看一个群聊,你在这个群聊里的网名叫{global_config.BOT_NICKNAME},你还有很多别名: {"/".join(global_config.BOT_ALIAS_NAMES)}, + 现在群里聊天的内容是{chat_talking_prompt}, + 今天是{current_date},现在是{current_time},你现在正在{bot_schedule_now_activity}。 + 综合群内的氛围和你自己之前的发言,给出你认为**最新的消息**需要你回复的概率,数值在0到1之间。请注意,群聊内容杂乱,很多时候对话连续,但很可能不是在和你说话。 + 如果最新的消息和你之前的发言在内容上连续,或者提到了你的名字或者称谓,将其视作明确指向你的互动,给出高于0.8的概率。如果现在是睡眠时间,直接概率为0。如果话题内容与你之前不是紧密相关,请不要给出高于0.1的概率。 + 请注意是判断概率,而不是编写回复内容, + 仅输出在0到1区间内的概率值,不要给出你的判断依据。 + """ + + # 非群聊的意愿管理 未来可能可以用对话缓冲区来确定合适的回复时机 + if not chat_in_group: + prompt = f""" + 假设你在和网友聊天,网名叫{global_config.BOT_NICKNAME},你还有很多别名: {"/".join(global_config.BOT_ALIAS_NAMES)}, + 现在你和朋友私聊的内容是{chat_talking_prompt}, + 今天是{current_date},现在是{current_time},你现在正在{bot_schedule_now_activity}。 + 综合以上的内容,给出你认为最新的消息是在和你交流的概率,数值在0到1之间。如果现在是个人休息时间,直接概率为0,请注意是决定是否需要发言,而不是编写回复内容, + 仅输出在0到1区间内的概率值,不要给出你的判断依据。 + """ + content_check, reasoning_check, _ = await self.model_v3.generate_response(prompt) + # logger.info(f"{prompt}") + logger.info(f"{content_check} {reasoning_check}") + probability = self.extract_marked_probability(content_check) + if probability <= 0.1: + probability = min(0.03, probability) + if probability >= 0.8: + probability = max(probability, 0.90) + # 兴趣系数修正 无关激活效率太高,暂时停用,待新记忆系统上线后调整 + # probability += (interested_rate * 0.25) + # probability = min(1.0, probability) + # 当前表情包理解能力较差,少说就少错 + if is_emoji: + probability *= 0.1 + + return probability + + @staticmethod + def extract_marked_probability(text): + """提取带标记的概率值 该方法主要用于测试微调prompt阶段""" + text = text.strip() + pattern = r'##PROBABILITY_START##(.*?)##PROBABILITY_END##' + match = re.search(pattern, text, re.DOTALL) + if match: + prob_str = match.group(1).strip() + # 处理百分比(65% → 0.65) + if '%' in prob_str: + return float(prob_str.replace('%', '')) / 100 + # 处理分数(2/3 → 0.666...) + elif '/' in prob_str: + numerator, denominator = map(float, prob_str.split('/')) + return numerator / denominator + # 直接处理小数 + else: + return float(prob_str) + + percent_match = re.search(r'(\d{1,3})%', text) # 65% + decimal_match = re.search(r'(0\.\d+|1\.0+)', text) # 0.65 + fraction_match = re.search(r'(\d+)/(\d+)', text) # 2/3 + try: + if percent_match: + prob = float(percent_match.group(1)) / 100 + elif decimal_match: + prob = float(decimal_match.group(0)) + elif fraction_match: + numerator, denominator = map(float, fraction_match.groups()) + prob = numerator / denominator + else: + return 0 # 无匹配格式 + + # 验证范围是否合法 + if 0 <= prob <= 1: + return prob + return 0 + except (ValueError, ZeroDivisionError): + return 0 + + def default_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, + **kwargs) -> float: + + current_willing = self.chat_reply_willing.get(chat_stream.stream_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.5 + self.chat_reply_willing[chat_stream.stream_id] = min(current_willing, 3.0) + reply_probability = min(max((current_willing - 0.5), 0.01) * config.response_willing_amplifier * 2, 1) + + return reply_probability From be3c1e167a5e73dbae8c70b7801e4c2945b03484 Mon Sep 17 00:00:00 2001 From: Ark-Hakobune Date: Mon, 14 Apr 2025 11:39:22 +0800 Subject: [PATCH 05/28] =?UTF-8?q?=E5=AE=8C=E6=88=90Maicore=E9=80=82?= =?UTF-8?q?=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 28 ++++++++++++++++++++++++++++ src/plugins/willing/mode_llmcheck.py | 15 +++++++-------- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 000d00c35..807114674 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,9 +25,37 @@ services: # ports: # - "8000:8000" volumes: +<<<<<<< HEAD - ./docker-config/mmc/.env:/MaiMBot/.env # 持久化env配置文件 - ./docker-config/mmc:/MaiMBot/config # 持久化bot配置文件 - ./data/MaiMBot:/MaiMBot/data # NapCat 和 NoneBot 共享此卷,否则发送图片会有问题 +======= + - ./config_ai/.env:/MaiMBot/.env # 持久化env配置文件 + - ./config_ai:/MaiMBot/config # 持久化bot配置文件 + - ./data_ai:/MaiMBot/data # NapCat 和 NoneBot 共享此卷,否则发送图片会有问题 + - ./src:/MaiMBot/src + restart: always + depends_on: + - mongodb + networks: + - maim_bot + + core_reimu: + container_name: reimu + image: mmc:local + # image: infinitycat/maimbot:main + environment: + - TZ=Asia/Shanghai + - EULA_AGREE=35362b6ea30f12891d46ef545122e84a # 同意EULA + - PRIVACY_AGREE=2402af06e133d2d10d9c6c643fdc9333 # 同意EULA + # ports: + # - "8001:8000" + volumes: + - ./config_reimu/.env:/MaiMBot/.env # 持久化env配置文件 + - ./config_reimu:/MaiMBot/config # 持久化bot配置文件 + - ./data_reimu/MaiMBot:/MaiMBot/data # NapCat 和 NoneBot 共享此卷,否则发送图片会有问题 + - ./src:/MaiMBot/src +>>>>>>> 7535f02 (完成Maicore适配) restart: always depends_on: - mongodb diff --git a/src/plugins/willing/mode_llmcheck.py b/src/plugins/willing/mode_llmcheck.py index 62a8cedba..6c3e135f9 100644 --- a/src/plugins/willing/mode_llmcheck.py +++ b/src/plugins/willing/mode_llmcheck.py @@ -22,7 +22,7 @@ chat_config = LogConfig( # 配置主程序日志格式 logger = get_module_logger("llm_willing", config=chat_config) -class WillingManager_llm(WillingManager): +class WillingManager(WillingManager): def __init__(self): super().__init__() @@ -39,8 +39,6 @@ class WillingManager_llm(WillingManager): 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 chat_talking_prompt = "" if stream_id: @@ -71,7 +69,7 @@ class WillingManager_llm(WillingManager): prompt = f""" 假设你正在查看一个群聊,你在这个群聊里的网名叫{global_config.BOT_NICKNAME},你还有很多别名: {"/".join(global_config.BOT_ALIAS_NAMES)}, 现在群里聊天的内容是{chat_talking_prompt}, - 今天是{current_date},现在是{current_time},你现在正在{bot_schedule_now_activity}。 + 今天是{current_date},现在是{current_time}。 综合群内的氛围和你自己之前的发言,给出你认为**最新的消息**需要你回复的概率,数值在0到1之间。请注意,群聊内容杂乱,很多时候对话连续,但很可能不是在和你说话。 如果最新的消息和你之前的发言在内容上连续,或者提到了你的名字或者称谓,将其视作明确指向你的互动,给出高于0.8的概率。如果现在是睡眠时间,直接概率为0。如果话题内容与你之前不是紧密相关,请不要给出高于0.1的概率。 请注意是判断概率,而不是编写回复内容, @@ -83,7 +81,7 @@ class WillingManager_llm(WillingManager): prompt = f""" 假设你在和网友聊天,网名叫{global_config.BOT_NICKNAME},你还有很多别名: {"/".join(global_config.BOT_ALIAS_NAMES)}, 现在你和朋友私聊的内容是{chat_talking_prompt}, - 今天是{current_date},现在是{current_time},你现在正在{bot_schedule_now_activity}。 + 今天是{current_date},现在是{current_time}。 综合以上的内容,给出你认为最新的消息是在和你交流的概率,数值在0到1之间。如果现在是个人休息时间,直接概率为0,请注意是决定是否需要发言,而不是编写回复内容, 仅输出在0到1区间内的概率值,不要给出你的判断依据。 """ @@ -91,13 +89,14 @@ class WillingManager_llm(WillingManager): # logger.info(f"{prompt}") logger.info(f"{content_check} {reasoning_check}") probability = self.extract_marked_probability(content_check) + # 兴趣系数修正 无关激活效率太高,暂时停用,待新记忆系统上线后调整 + probability += (interested_rate * 0.25) + probability = min(1.0, probability) if probability <= 0.1: probability = min(0.03, probability) if probability >= 0.8: probability = max(probability, 0.90) - # 兴趣系数修正 无关激活效率太高,暂时停用,待新记忆系统上线后调整 - # probability += (interested_rate * 0.25) - # probability = min(1.0, probability) + # 当前表情包理解能力较差,少说就少错 if is_emoji: probability *= 0.1 From 9dc4c4f5ac774e0183dfcf008dc6c453c257a827 Mon Sep 17 00:00:00 2001 From: Ark-Hakobune Date: Tue, 15 Apr 2025 22:13:18 +0800 Subject: [PATCH 06/28] =?UTF-8?q?chore:=20=E5=90=8C=E6=AD=A5=E4=B8=8A?= =?UTF-8?q?=E6=B8=B8=20README=20=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 36 ++++++++++-------------------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index beea5757f..325e3ad22 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@
- ![Python Version](https://img.shields.io/badge/Python-3.9+-blue) + ![Python Version](https://img.shields.io/badge/Python-3.10+-blue) ![License](https://img.shields.io/github/license/SengokuCola/MaiMBot?label=协议) ![Status](https://img.shields.io/badge/状态-开发中-yellow) ![Contributors](https://img.shields.io/github/contributors/MaiM-with-u/MaiBot.svg?style=flat&label=贡献者) @@ -37,7 +37,7 @@

-## 新版0.6.0部署前先阅读:https://docs.mai-mai.org/manual/usage/mmc_q_a +## 新版0.6.x部署前先阅读:https://docs.mai-mai.org/manual/usage/mmc_q_a ## 📝 项目简介 @@ -60,23 +60,6 @@
-### 📢 版本信息 - -- 💭 **智能对话系统**:基于LLM的自然语言交互 -- 🤔 **实时思维系统**:模拟人类思考过程 -- 💝 **情感表达系统**:丰富的表情包和情绪表达 -- 🧠 **持久记忆系统**:基于MongoDB的长期记忆存储 -- 🔄 **动态人格系统**:自适应的性格特征 - -
- - 麦麦演示视频 -
- 👆 点击观看麦麦演示视频 👆 -
-
- - ### 📢 版本信息 **最新版本: v0.6.2** ([查看更新日志](changelogs/changelog.md)) @@ -103,7 +86,7 @@ ### ⚠️ 重要提示 -- 升级到v0.6.0版本前请务必阅读:[升级指南](https://docs.mai-mai.org/manual/usage/mmc_q_a) +- 升级到v0.6.x版本前请务必阅读:[升级指南](https://docs.mai-mai.org/manual/usage/mmc_q_a) - 本版本基于MaiCore重构,通过nonebot插件与QQ平台交互 - 项目处于活跃开发阶段,功能和API可能随时调整 @@ -132,21 +115,22 @@ | 模块 | 主要功能 | 特点 | |------|---------|------| -| 💬 聊天系统 | • 思维流/推理聊天
• 关键词主动发言
• 多模型支持
• 动态prompt构建
• 私聊功能(PFC) | 拟人化交互 | -| 🧠 思维流系统 | • 实时思考生成
• 自动启停机制
• 日程系统联动 | 智能化决策 | -| 🧠 记忆系统 2.0 | • 优化记忆抽取
• 海马体记忆机制
• 聊天记录概括 | 持久化记忆 | -| 😊 表情包系统 | • 情绪匹配发送
• GIF支持
• 自动收集与审查 | 丰富表达 | +| 💬 聊天系统 | • 心流/推理聊天
• 关键词主动发言
• 多模型支持
• 动态prompt构建
• 私聊功能(PFC) | 拟人化交互 | +| 🧠 心流系统 | • 实时思考生成
• 自动启停机制
• 日程系统联动
• 工具调用能力 | 智能化决策 | +| 🧠 记忆系统 | • 优化记忆抽取
• 海马体记忆机制
• 聊天记录概括 | 持久化记忆 | +| 😊 表情系统 | • 情绪匹配发送
• GIF支持
• 自动收集与审查 | 丰富表达 | | 📅 日程系统 | • 动态日程生成
• 自定义想象力
• 思维流联动 | 智能规划 | -| 👥 关系系统 2.0 | • 关系管理优化
• 丰富接口支持
• 个性化交互 | 深度社交 | +| 👥 关系系统 | • 关系管理优化
• 丰富接口支持
• 个性化交互 | 深度社交 | | 📊 统计系统 | • 使用数据统计
• LLM调用记录
• 实时控制台显示 | 数据可视 | | 🔧 系统功能 | • 优雅关闭机制
• 自动数据保存
• 异常处理完善 | 稳定可靠 | +| 🛠️ 工具系统 | • 知识获取工具
• 自动注册机制
• 多工具支持 | 扩展功能 | ## 📐 项目架构 ```mermaid graph TD A[MaiCore] --> B[对话系统] - A --> C[思维流系统] + A --> C[心流系统] A --> D[记忆系统] A --> E[情感系统] B --> F[多模型支持] From 307f6707191d7c92b149babeedd0256b7df67f35 Mon Sep 17 00:00:00 2001 From: Ark-Hakobune Date: Tue, 15 Apr 2025 22:15:19 +0800 Subject: [PATCH 07/28] =?UTF-8?q?chore:=20=E5=90=8C=E6=AD=A5=E4=B8=8A?= =?UTF-8?q?=E6=B8=B8docker-compose=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 807114674..000d00c35 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,37 +25,9 @@ services: # ports: # - "8000:8000" volumes: -<<<<<<< HEAD - ./docker-config/mmc/.env:/MaiMBot/.env # 持久化env配置文件 - ./docker-config/mmc:/MaiMBot/config # 持久化bot配置文件 - ./data/MaiMBot:/MaiMBot/data # NapCat 和 NoneBot 共享此卷,否则发送图片会有问题 -======= - - ./config_ai/.env:/MaiMBot/.env # 持久化env配置文件 - - ./config_ai:/MaiMBot/config # 持久化bot配置文件 - - ./data_ai:/MaiMBot/data # NapCat 和 NoneBot 共享此卷,否则发送图片会有问题 - - ./src:/MaiMBot/src - restart: always - depends_on: - - mongodb - networks: - - maim_bot - - core_reimu: - container_name: reimu - image: mmc:local - # image: infinitycat/maimbot:main - environment: - - TZ=Asia/Shanghai - - EULA_AGREE=35362b6ea30f12891d46ef545122e84a # 同意EULA - - PRIVACY_AGREE=2402af06e133d2d10d9c6c643fdc9333 # 同意EULA - # ports: - # - "8001:8000" - volumes: - - ./config_reimu/.env:/MaiMBot/.env # 持久化env配置文件 - - ./config_reimu:/MaiMBot/config # 持久化bot配置文件 - - ./data_reimu/MaiMBot:/MaiMBot/data # NapCat 和 NoneBot 共享此卷,否则发送图片会有问题 - - ./src:/MaiMBot/src ->>>>>>> 7535f02 (完成Maicore适配) restart: always depends_on: - mongodb From 195fc7327cb71d142e601189334f63d4bf9153f9 Mon Sep 17 00:00:00 2001 From: meng_xi_pan Date: Tue, 15 Apr 2025 22:39:33 +0800 Subject: [PATCH 08/28] =?UTF-8?q?=E9=87=8D=E6=9E=84mxp=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E7=9A=84=E5=9F=BA=E7=A1=80=E6=84=8F=E6=84=BF=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/willing/mode_classical.py | 5 ---- src/plugins/willing/mode_dynamic.py | 5 ---- src/plugins/willing/mode_mxp.py | 41 +++++++------------------- src/plugins/willing/willing_manager.py | 20 ++++++------- 4 files changed, 21 insertions(+), 50 deletions(-) diff --git a/src/plugins/willing/mode_classical.py b/src/plugins/willing/mode_classical.py index 294539d08..b74666215 100644 --- a/src/plugins/willing/mode_classical.py +++ b/src/plugins/willing/mode_classical.py @@ -75,8 +75,3 @@ class ClassicalWillingManager(BaseWillingManager): async def not_reply_handle(self, message_id): return await super().not_reply_handle(message_id) - async def get_variable_parameters(self): - return await super().get_variable_parameters() - - async def set_variable_parameters(self, parameters): - return await super().set_variable_parameters(parameters) diff --git a/src/plugins/willing/mode_dynamic.py b/src/plugins/willing/mode_dynamic.py index 0487a1a98..1a5ebbd15 100644 --- a/src/plugins/willing/mode_dynamic.py +++ b/src/plugins/willing/mode_dynamic.py @@ -235,8 +235,3 @@ class DynamicWillingManager(BaseWillingManager): async def after_generate_reply_handle(self, message_id): return await super().after_generate_reply_handle(message_id) - async def get_variable_parameters(self): - return await super().get_variable_parameters() - - async def set_variable_parameters(self, parameters): - return await super().set_variable_parameters(parameters) diff --git a/src/plugins/willing/mode_mxp.py b/src/plugins/willing/mode_mxp.py index b4fc1448c..05b6c7e5c 100644 --- a/src/plugins/willing/mode_mxp.py +++ b/src/plugins/willing/mode_mxp.py @@ -37,8 +37,8 @@ class MxpWillingManager(BaseWillingManager): # 可变参数 self.intention_decay_rate = 0.93 # 意愿衰减率 - self.message_expiration_time = 120 # 消息过期时间(秒) - self.number_of_message_storage = 10 # 消息存储数量 + self.number_of_message_storage = 12 # 消息存储数量 + self.expected_replies_per_min = 3 # 每分钟预期回复数 self.basic_maximum_willing = 0.5 # 基础最大意愿值 self.mention_willing_gain = 0.6 # 提及意愿增益 self.interest_willing_gain = 0.3 # 兴趣意愿增益 @@ -193,7 +193,8 @@ class MxpWillingManager(BaseWillingManager): # 清理过期消息 current_time = time.time() message_times = [ - msg_time for msg_time in message_times if current_time - msg_time < self.message_expiration_time + msg_time for msg_time in message_times if current_time - msg_time < + self.number_of_message_storage * self.basic_maximum_willing / self.expected_replies_per_min * 60 ] self.chat_new_message_time[chat_id] = message_times @@ -202,38 +203,13 @@ class MxpWillingManager(BaseWillingManager): update_time = 20 elif len(message_times) == self.number_of_message_storage: time_interval = current_time - message_times[0] - basic_willing = self.basic_maximum_willing * math.sqrt( - time_interval / self.message_expiration_time - ) + basic_willing = self._basic_willing_coefficient_culculate(time_interval) self.chat_reply_willing[chat_id] = basic_willing - update_time = 17 * math.sqrt(time_interval / self.message_expiration_time) + 3 + update_time = 17 * basic_willing / self.basic_maximum_willing + 3 else: self.logger.debug(f"聊天流{chat_id}消息时间数量异常,数量:{len(message_times)}") self.chat_reply_willing[chat_id] = 0 - async def get_variable_parameters(self) -> Dict[str, str]: - """获取可变参数""" - return { - "intention_decay_rate": "意愿衰减率", - "message_expiration_time": "消息过期时间(秒)", - "number_of_message_storage": "消息存储数量", - "basic_maximum_willing": "基础最大意愿值", - "mention_willing_gain": "提及意愿增益", - "interest_willing_gain": "兴趣意愿增益", - "emoji_response_penalty": "表情包回复惩罚", - "down_frequency_rate": "降低回复频率的群组惩罚系数", - "single_chat_gain": "单聊增益(不仅是私聊)", - } - - async def set_variable_parameters(self, parameters: Dict[str, any]): - """设置可变参数""" - async with self.lock: - for key, value in parameters.items(): - if hasattr(self, key): - setattr(self, key, value) - self.logger.debug(f"参数 {key} 已更新为 {value}") - else: - self.logger.debug(f"尝试设置未知参数 {key}") def _get_relationship_level_num(self, relationship_value) -> int: """关系等级计算""" @@ -253,5 +229,10 @@ class MxpWillingManager(BaseWillingManager): level_num = 5 if relationship_value > 1000 else 0 return level_num - 2 + def _basic_willing_coefficient_culculate(self, t: float) -> float: + """基础意愿值系数计算""" + return math.tan(t * self.expected_replies_per_min * math.pi + / 120 / self.number_of_message_storage) / 2 + async def get_willing(self, chat_id): return self.temporary_willing diff --git a/src/plugins/willing/willing_manager.py b/src/plugins/willing/willing_manager.py index ada995120..28185bff1 100644 --- a/src/plugins/willing/willing_manager.py +++ b/src/plugins/willing/willing_manager.py @@ -18,8 +18,8 @@ after_generate_reply_handle 确定要回复后,在生成回复后的处理 not_reply_handle 确定不回复后的处理 get_reply_probability 获取回复概率 bombing_buffer_message_handle 缓冲器炸飞消息后的处理 -get_variable_parameters 获取可变参数组,返回一个字典,key为参数名称,value为参数描述(此方法是为拆分全局设置准备) -set_variable_parameters 设置可变参数组,你需要传入一个字典,key为参数名称,value为参数值(此方法是为拆分全局设置准备) +get_variable_parameters 暂不确定 +set_variable_parameters 暂不确定 以下2个方法根据你的实现可以做调整: get_willing 获取某聊天流意愿 set_willing 设置某聊天流意愿 @@ -152,15 +152,15 @@ class BaseWillingManager(ABC): async with self.lock: self.chat_reply_willing[chat_id] = willing - @abstractmethod - async def get_variable_parameters(self) -> Dict[str, str]: - """抽象方法:获取可变参数""" - pass + # @abstractmethod + # async def get_variable_parameters(self) -> Dict[str, str]: + # """抽象方法:获取可变参数""" + # pass - @abstractmethod - async def set_variable_parameters(self, parameters: Dict[str, any]): - """抽象方法:设置可变参数""" - pass + # @abstractmethod + # async def set_variable_parameters(self, parameters: Dict[str, any]): + # """抽象方法:设置可变参数""" + # pass def init_willing_manager() -> BaseWillingManager: From 7b261078b5877e091c8c5e396d58bf7b99d220c0 Mon Sep 17 00:00:00 2001 From: Ark-Hakobune Date: Tue, 15 Apr 2025 22:50:04 +0800 Subject: [PATCH 09/28] =?UTF-8?q?0.6.2=E7=9A=84=E6=84=8F=E6=84=BF=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/willing/mode_llmcheck.py | 141 +++++++++++++-------------- 1 file changed, 69 insertions(+), 72 deletions(-) diff --git a/src/plugins/willing/mode_llmcheck.py b/src/plugins/willing/mode_llmcheck.py index 6c3e135f9..4f472e87f 100644 --- a/src/plugins/willing/mode_llmcheck.py +++ b/src/plugins/willing/mode_llmcheck.py @@ -1,68 +1,87 @@ +""" +llmcheck 模式: +此模式的一些参数不会在配置文件中显示,要修改请在可变参数下修改 +此模式的特点: +1.在群聊内的连续对话场景下,使用大语言模型来判断回复概率 +2.非连续对话场景,使用mxp模式的意愿管理器(可另外配置) +3.默认配置的是model_v3,当前参数适用于deepseek-v3-0324 + +继承自其他模式,实质上仅重写get_reply_probability方法,未来可能重构成一个插件,可方便地组装到其他意愿模式上。 +目前的使用方式是拓展到其他意愿管理模式 + +""" import time from loguru import logger -from ..schedule.schedule_generator import bot_schedule from ..models.utils_model import LLM_request - from ..config.config import global_config from ..chat.chat_stream import ChatStream -from .mode_classical import WillingManager from ..chat.utils import get_recent_group_detailed_plain_text - - +from .willing_manager import BaseWillingManager +from .mode_mxp import MxpWillingManager import re -from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig +from functools import wraps -# 定义日志配置 -chat_config = LogConfig( - # 使用消息发送专用样式 - console_format=CHAT_STYLE_CONFIG["console_format"], - file_format=CHAT_STYLE_CONFIG["file_format"], -) -# 配置主程序日志格式 -logger = get_module_logger("llm_willing", config=chat_config) +def is_continuous_chat(self, message_id: str): + # 判断是否是连续对话,出于成本考虑,默认限制5条 + willing_info = self.ongoing_messages[message_id] + chat_id = willing_info.chat_id + group_info = willing_info.chat_id + config = self.global_config + length = 5 + if chat_id: + chat_talking_text = get_recent_group_detailed_plain_text( + chat_id, limit=length, combine=True + ) + if group_info: + if str(config.BOT_QQ) in chat_talking_text: + return True + else: + return False + return False -class WillingManager(WillingManager): +def llmcheck_decorator(trigger_condition_func): + def decorator(func): + @wraps(func) + def wrapper(self, message_id: str): + if trigger_condition_func(self, message_id): + # 满足条件,走llm流程 + return self.get_llmreply_probability(message_id) + else: + # 不满足条件,走默认流程 + return func(self, message_id) + return wrapper + return decorator + + +class LlmcheckWillingManager(MxpWillingManager): def __init__(self): super().__init__() self.model_v3 = LLM_request(model=global_config.llm_normal, temperature=0.3) - 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, - **kwargs) -> float: - stream_id = chat_stream.stream_id - if chat_stream.group_info and config: - if chat_stream.group_info.group_id not in config.talk_allowed_groups: + + + async def get_llmreply_probability(self, message_id: str): + message_info = self.ongoing_messages[message_id] + chat_id = message_info.chat_id + config = self.global_config + # 获取信息的长度 + length = 5 + if message_info.group_info and config: + if message_info.group_info.group_id not in config.talk_allowed_groups: reply_probability = 0 return reply_probability current_date = time.strftime("%Y-%m-%d", time.localtime()) current_time = time.strftime("%H:%M:%S", time.localtime()) - chat_in_group = True chat_talking_prompt = "" - if stream_id: + if chat_id: chat_talking_prompt = get_recent_group_detailed_plain_text( - stream_id, limit=5, combine=True + chat_id, limit=length, combine=True ) - if chat_stream.group_info: - if str(config.BOT_QQ) in chat_talking_prompt: - pass - # logger.info(f"{chat_talking_prompt}") - # logger.info(f"bot在群聊中5条内发过言,启动llm计算回复概率") - else: - return self.default_change_reply_willing_received( - chat_stream=chat_stream, - is_mentioned_bot=is_mentioned_bot, - config=config, - is_emoji=is_emoji, - interested_rate=interested_rate, - sender_id=sender_id, - ) - else: - chat_in_group = False - chat_talking_prompt = chat_talking_prompt - # print(f"\033[1;34m[调试]\033[0m 已从数据库获取群 {group_id} 的消息记录:{chat_talking_prompt}") + else: + return 0 # if is_mentioned_bot: # return 1.0 @@ -76,21 +95,12 @@ class WillingManager(WillingManager): 仅输出在0到1区间内的概率值,不要给出你的判断依据。 """ - # 非群聊的意愿管理 未来可能可以用对话缓冲区来确定合适的回复时机 - if not chat_in_group: - prompt = f""" - 假设你在和网友聊天,网名叫{global_config.BOT_NICKNAME},你还有很多别名: {"/".join(global_config.BOT_ALIAS_NAMES)}, - 现在你和朋友私聊的内容是{chat_talking_prompt}, - 今天是{current_date},现在是{current_time}。 - 综合以上的内容,给出你认为最新的消息是在和你交流的概率,数值在0到1之间。如果现在是个人休息时间,直接概率为0,请注意是决定是否需要发言,而不是编写回复内容, - 仅输出在0到1区间内的概率值,不要给出你的判断依据。 - """ content_check, reasoning_check, _ = await self.model_v3.generate_response(prompt) # logger.info(f"{prompt}") logger.info(f"{content_check} {reasoning_check}") probability = self.extract_marked_probability(content_check) # 兴趣系数修正 无关激活效率太高,暂时停用,待新记忆系统上线后调整 - probability += (interested_rate * 0.25) + probability += (message_info.interested_rate * 0.25) probability = min(1.0, probability) if probability <= 0.1: probability = min(0.03, probability) @@ -98,8 +108,8 @@ class WillingManager(WillingManager): probability = max(probability, 0.90) # 当前表情包理解能力较差,少说就少错 - if is_emoji: - probability *= 0.1 + if message_info.is_emoji: + probability *= global_config.emoji_response_penalty return probability @@ -143,21 +153,8 @@ class WillingManager(WillingManager): except (ValueError, ZeroDivisionError): return 0 - def default_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, - **kwargs) -> float: - - current_willing = self.chat_reply_willing.get(chat_stream.stream_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.5 - self.chat_reply_willing[chat_stream.stream_id] = min(current_willing, 3.0) - reply_probability = min(max((current_willing - 0.5), 0.01) * config.response_willing_amplifier * 2, 1) - - return reply_probability + @llmcheck_decorator(is_continuous_chat) + def get_reply_probability(self, message_id): + return super().get_reply_probability( + message_id + ) From 37c58e8b7a02de9ad7ece253901e5cc523b56986 Mon Sep 17 00:00:00 2001 From: Ark-Hakobune Date: Tue, 15 Apr 2025 23:02:43 +0800 Subject: [PATCH 10/28] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=BD=B1=E5=93=8D?= =?UTF-8?q?=E7=A7=81=E8=81=8A=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/willing/mode_llmcheck.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/willing/mode_llmcheck.py b/src/plugins/willing/mode_llmcheck.py index 4f472e87f..d4e867a37 100644 --- a/src/plugins/willing/mode_llmcheck.py +++ b/src/plugins/willing/mode_llmcheck.py @@ -26,7 +26,7 @@ def is_continuous_chat(self, message_id: str): # 判断是否是连续对话,出于成本考虑,默认限制5条 willing_info = self.ongoing_messages[message_id] chat_id = willing_info.chat_id - group_info = willing_info.chat_id + group_info = willing_info.group_info config = self.global_config length = 5 if chat_id: From 21179884cfd2396ba49a8e762839b41d0652c76a Mon Sep 17 00:00:00 2001 From: meng_xi_pan Date: Tue, 15 Apr 2025 23:55:15 +0800 Subject: [PATCH 11/28] =?UTF-8?q?=E7=96=B2=E5=8A=B3=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E5=88=9D=E6=AD=A5=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/willing/mode_mxp.py | 51 +++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/src/plugins/willing/mode_mxp.py b/src/plugins/willing/mode_mxp.py index 05b6c7e5c..3fb1d7db2 100644 --- a/src/plugins/willing/mode_mxp.py +++ b/src/plugins/willing/mode_mxp.py @@ -10,6 +10,7 @@ Mxp 模式:梦溪畔独家赞助 4.限制同时思考的消息数量,防止喷射 5.拥有单聊增益,无论在群里还是私聊,只要bot一直和你聊,就会增加意愿值 6.意愿分为衰减意愿+临时意愿 +7.疲劳机制 如果你发现本模式出现了bug 上上策是询问智慧的小草神() @@ -34,26 +35,47 @@ class MxpWillingManager(BaseWillingManager): self.chat_new_message_time: Dict[str, list[float]] = {} # 聊天流ID: 消息时间 self.last_response_person: Dict[str, tuple[str, int]] = {} # 上次回复的用户信息 self.temporary_willing: float = 0 # 临时意愿值 + self.chat_bot_message_time: Dict[str, list[float]] = {} # 聊天流ID: bot已回复消息时间 + self.chat_fatigue_punishment_list: Dict[str, list[tuple[float, float]]] = {} # 聊天流疲劳惩罚列, 聊天流ID: 惩罚时间列(开始时间,持续时间) + self.chat_fatigue_willing_attenuation: Dict[str, float] = {} # 聊天流疲劳意愿衰减值 # 可变参数 self.intention_decay_rate = 0.93 # 意愿衰减率 + self.number_of_message_storage = 12 # 消息存储数量 self.expected_replies_per_min = 3 # 每分钟预期回复数 self.basic_maximum_willing = 0.5 # 基础最大意愿值 + self.mention_willing_gain = 0.6 # 提及意愿增益 self.interest_willing_gain = 0.3 # 兴趣意愿增益 self.emoji_response_penalty = self.global_config.emoji_response_penalty # 表情包回复惩罚 self.down_frequency_rate = self.global_config.down_frequency_rate # 降低回复频率的群组惩罚系数 self.single_chat_gain = 0.12 # 单聊增益 + self.fatigue_messages_triggered_num = self.expected_replies_per_min # 疲劳消息触发数量(int) + self.fatigue_coefficient = 1.0 # 疲劳系数 + async def async_task_starter(self) -> None: """异步任务启动器""" asyncio.create_task(self._return_to_basic_willing()) asyncio.create_task(self._chat_new_message_to_change_basic_willing()) + asyncio.create_task(self.fatigue_attenuation()) async def before_generate_reply_handle(self, message_id: str): """回复前处理""" - pass + current_time = time.time() + async with self.lock: + w_info = self.ongoing_messages[message_id] + if w_info.chat_id not in self.chat_bot_message_time: + self.chat_bot_message_time[w_info.chat_id] = [] + self.chat_bot_message_time[w_info.chat_id] = \ + [t for t in self.chat_bot_message_time[w_info.chat_id] if current_time - t < 60] + self.chat_bot_message_time[w_info.chat_id].append(current_time) + if len(self.chat_bot_message_time[w_info.chat_id]) == int(self.fatigue_messages_triggered_num): + time_interval = 60 - (current_time - self.chat_bot_message_time[w_info.chat_id].pop(0)) + if w_info.chat_id not in self.chat_fatigue_punishment_list: + self.chat_fatigue_punishment_list[w_info.chat_id] = [] + self.chat_fatigue_punishment_list[w_info.chat_id].append(current_time, time_interval * 2) async def after_generate_reply_handle(self, message_id: str): """回复后处理""" @@ -122,6 +144,8 @@ class MxpWillingManager(BaseWillingManager): elif len(chat_ongoing_messages) >= 4: current_willing = 0 + current_willing += self.chat_fatigue_willing_attenuation.get(w_info.chat_id, 0) + probability = self._willing_to_probability(current_willing) if w_info.is_emoji: @@ -179,8 +203,10 @@ class MxpWillingManager(BaseWillingManager): willing = max(0, willing) if willing < 2: probability = math.atan(willing * 2) / math.pi * 2 - else: + elif willing <2.5: probability = math.atan(willing * 4) / math.pi * 2 + else: + probability = 1 return probability async def _chat_new_message_to_change_basic_willing(self): @@ -203,7 +229,7 @@ class MxpWillingManager(BaseWillingManager): update_time = 20 elif len(message_times) == self.number_of_message_storage: time_interval = current_time - message_times[0] - basic_willing = self._basic_willing_coefficient_culculate(time_interval) + basic_willing = self._basic_willing_culculate(time_interval) self.chat_reply_willing[chat_id] = basic_willing update_time = 17 * basic_willing / self.basic_maximum_willing + 3 else: @@ -229,10 +255,25 @@ class MxpWillingManager(BaseWillingManager): level_num = 5 if relationship_value > 1000 else 0 return level_num - 2 - def _basic_willing_coefficient_culculate(self, t: float) -> float: - """基础意愿值系数计算""" + def _basic_willing_culculate(self, t: float) -> float: + """基础意愿值计算""" return math.tan(t * self.expected_replies_per_min * math.pi / 120 / self.number_of_message_storage) / 2 + + async def fatigue_attenuation(self): + """疲劳衰减""" + while True: + current_time = time.time() + await asyncio.sleep(1) + async with self.lock: + for chat_id, fatigue_list in self.chat_fatigue_punishment_list.items(): + fatigue_list = [z for z in fatigue_list if current_time - z[0] < z[1]] + self.chat_fatigue_willing_attenuation[chat_id] = 0 + for start_time, duration in fatigue_list: + self.chat_fatigue_willing_attenuation[chat_id] += \ + (self.chat_reply_willing[chat_id] * 2 / math.pi * math.asin( + 2 * (current_time - start_time) / duration - 1 + ) - self.chat_reply_willing[chat_id]) * self.fatigue_coefficient async def get_willing(self, chat_id): return self.temporary_willing From 10c89c0171d656247a1bc39b2c74eee9de2a5060 Mon Sep 17 00:00:00 2001 From: meng_xi_pan Date: Wed, 16 Apr 2025 00:08:22 +0800 Subject: [PATCH 12/28] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=B0=83=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/willing/mode_mxp.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/plugins/willing/mode_mxp.py b/src/plugins/willing/mode_mxp.py index 3fb1d7db2..e0e233aac 100644 --- a/src/plugins/willing/mode_mxp.py +++ b/src/plugins/willing/mode_mxp.py @@ -55,6 +55,8 @@ class MxpWillingManager(BaseWillingManager): self.fatigue_messages_triggered_num = self.expected_replies_per_min # 疲劳消息触发数量(int) self.fatigue_coefficient = 1.0 # 疲劳系数 + self.is_debug = False # 是否开启调试模式 + async def async_task_starter(self) -> None: """异步任务启动器""" asyncio.create_task(self._return_to_basic_willing()) @@ -114,24 +116,38 @@ class MxpWillingManager(BaseWillingManager): async with self.lock: w_info = self.ongoing_messages[message_id] current_willing = self.chat_person_reply_willing[w_info.chat_id][w_info.person_id] + if self.is_debug: + self.logger.debug(f"基础意愿值:{current_willing}") if w_info.is_mentioned_bot: current_willing += self.mention_willing_gain / (int(current_willing) + 1) + if self.is_debug: + self.logger.debug(f"提及增益:{self.mention_willing_gain / (int(current_willing) + 1)}") if w_info.interested_rate > 0: current_willing += math.atan(w_info.interested_rate / 2) / math.pi * 2 * self.interest_willing_gain + if self.is_debug: + self.logger.debug(f"兴趣增益:{math.atan(w_info.interested_rate / 2) / math.pi * 2 * self.interest_willing_gain}") self.chat_person_reply_willing[w_info.chat_id][w_info.person_id] = current_willing rel_value = await w_info.person_info_manager.get_value(w_info.person_id, "relationship_value") rel_level = self._get_relationship_level_num(rel_value) current_willing += rel_level * 0.1 + if self.is_debug: + self.logger.debug(f"关系增益:{rel_level * 0.1}") if ( w_info.chat_id in self.last_response_person and self.last_response_person[w_info.chat_id][0] == w_info.person_id ): current_willing += self.single_chat_gain * (2 * self.last_response_person[w_info.chat_id][1] + 1) + if self.is_debug: + self.logger.debug(f"单聊增益:{self.single_chat_gain * (2 * self.last_response_person[w_info.chat_id][1] + 1)}") + + current_willing += self.chat_fatigue_willing_attenuation.get(w_info.chat_id, 0) + if self.is_debug: + self.logger.debug(f"疲劳衰减:{self.chat_fatigue_willing_attenuation.get(w_info.chat_id, 0)}") chat_ongoing_messages = [msg for msg in self.ongoing_messages.values() if msg.chat_id == w_info.chat_id] chat_person_ogoing_messages = [msg for msg in chat_ongoing_messages if msg.person_id == w_info.person_id] @@ -143,8 +159,10 @@ class MxpWillingManager(BaseWillingManager): current_willing -= 1.5 elif len(chat_ongoing_messages) >= 4: current_willing = 0 - - current_willing += self.chat_fatigue_willing_attenuation.get(w_info.chat_id, 0) + else: + if self.is_debug: + self.logger.debug("无进行中消息惩罚") + probability = self._willing_to_probability(current_willing) From a20ca1d0f2bfd5d6aca2ad94b880f74e04e34c3c Mon Sep 17 00:00:00 2001 From: meng_xi_pan Date: Wed, 16 Apr 2025 01:43:36 +0800 Subject: [PATCH 13/28] =?UTF-8?q?=E4=BF=AE=E6=94=B9bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/willing/mode_mxp.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/plugins/willing/mode_mxp.py b/src/plugins/willing/mode_mxp.py index e0e233aac..f2df94288 100644 --- a/src/plugins/willing/mode_mxp.py +++ b/src/plugins/willing/mode_mxp.py @@ -61,7 +61,7 @@ class MxpWillingManager(BaseWillingManager): """异步任务启动器""" asyncio.create_task(self._return_to_basic_willing()) asyncio.create_task(self._chat_new_message_to_change_basic_willing()) - asyncio.create_task(self.fatigue_attenuation()) + asyncio.create_task(self._fatigue_attenuation()) async def before_generate_reply_handle(self, message_id: str): """回复前处理""" @@ -77,7 +77,7 @@ class MxpWillingManager(BaseWillingManager): time_interval = 60 - (current_time - self.chat_bot_message_time[w_info.chat_id].pop(0)) if w_info.chat_id not in self.chat_fatigue_punishment_list: self.chat_fatigue_punishment_list[w_info.chat_id] = [] - self.chat_fatigue_punishment_list[w_info.chat_id].append(current_time, time_interval * 2) + self.chat_fatigue_punishment_list[w_info.chat_id].append([current_time, time_interval * 2]) async def after_generate_reply_handle(self, message_id: str): """回复后处理""" @@ -120,9 +120,10 @@ class MxpWillingManager(BaseWillingManager): self.logger.debug(f"基础意愿值:{current_willing}") if w_info.is_mentioned_bot: - current_willing += self.mention_willing_gain / (int(current_willing) + 1) + current_willing_ = self.mention_willing_gain / (int(current_willing) + 1) + current_willing += current_willing_ if self.is_debug: - self.logger.debug(f"提及增益:{self.mention_willing_gain / (int(current_willing) + 1)}") + self.logger.debug(f"提及增益:{current_willing_}") if w_info.interested_rate > 0: current_willing += math.atan(w_info.interested_rate / 2) / math.pi * 2 * self.interest_willing_gain @@ -153,15 +154,20 @@ class MxpWillingManager(BaseWillingManager): chat_person_ogoing_messages = [msg for msg in chat_ongoing_messages if msg.person_id == w_info.person_id] if len(chat_person_ogoing_messages) >= 2: current_willing = 0 + if self.is_debug: + self.logger.debug("进行中消息惩罚:归0") elif len(chat_ongoing_messages) == 2: current_willing -= 0.5 + if self.is_debug: + self.logger.debug("进行中消息惩罚:-0.5") elif len(chat_ongoing_messages) == 3: current_willing -= 1.5 + if self.is_debug: + self.logger.debug("进行中消息惩罚:-1.5") elif len(chat_ongoing_messages) >= 4: current_willing = 0 - else: if self.is_debug: - self.logger.debug("无进行中消息惩罚") + self.logger.debug("进行中消息惩罚:归0") probability = self._willing_to_probability(current_willing) @@ -229,8 +235,8 @@ class MxpWillingManager(BaseWillingManager): async def _chat_new_message_to_change_basic_willing(self): """聊天流新消息改变基础意愿""" + update_time = 20 while True: - update_time = 20 await asyncio.sleep(update_time) async with self.lock: for chat_id, message_times in self.chat_new_message_time.items(): @@ -253,6 +259,8 @@ class MxpWillingManager(BaseWillingManager): else: self.logger.debug(f"聊天流{chat_id}消息时间数量异常,数量:{len(message_times)}") self.chat_reply_willing[chat_id] = 0 + if self.is_debug: + self.logger.debug(f"聊天流意愿值更新:{self.chat_reply_willing}") def _get_relationship_level_num(self, relationship_value) -> int: @@ -278,11 +286,11 @@ class MxpWillingManager(BaseWillingManager): return math.tan(t * self.expected_replies_per_min * math.pi / 120 / self.number_of_message_storage) / 2 - async def fatigue_attenuation(self): + async def _fatigue_attenuation(self): """疲劳衰减""" while True: - current_time = time.time() await asyncio.sleep(1) + current_time = time.time() async with self.lock: for chat_id, fatigue_list in self.chat_fatigue_punishment_list.items(): fatigue_list = [z for z in fatigue_list if current_time - z[0] < z[1]] From 0bba90b48cb011ef50f8d043ef22de78d5c17239 Mon Sep 17 00:00:00 2001 From: meng_xi_pan Date: Wed, 16 Apr 2025 10:31:27 +0800 Subject: [PATCH 14/28] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E8=A1=B0=E5=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/willing/mode_mxp.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/plugins/willing/mode_mxp.py b/src/plugins/willing/mode_mxp.py index f2df94288..f063f6f24 100644 --- a/src/plugins/willing/mode_mxp.py +++ b/src/plugins/willing/mode_mxp.py @@ -75,8 +75,6 @@ class MxpWillingManager(BaseWillingManager): self.chat_bot_message_time[w_info.chat_id].append(current_time) if len(self.chat_bot_message_time[w_info.chat_id]) == int(self.fatigue_messages_triggered_num): time_interval = 60 - (current_time - self.chat_bot_message_time[w_info.chat_id].pop(0)) - if w_info.chat_id not in self.chat_fatigue_punishment_list: - self.chat_fatigue_punishment_list[w_info.chat_id] = [] self.chat_fatigue_punishment_list[w_info.chat_id].append([current_time, time_interval * 2]) async def after_generate_reply_handle(self, message_id: str): @@ -99,7 +97,7 @@ class MxpWillingManager(BaseWillingManager): async with self.lock: w_info = self.ongoing_messages[message_id] if w_info.is_mentioned_bot: - self.chat_person_reply_willing[w_info.chat_id][w_info.person_id] += 0.2 + self.chat_person_reply_willing[w_info.chat_id][w_info.person_id] += self.mention_willing_gain / 2.5 if ( w_info.chat_id in self.last_response_person and self.last_response_person[w_info.chat_id][0] == w_info.person_id @@ -216,12 +214,17 @@ class MxpWillingManager(BaseWillingManager): self.ongoing_messages[message.message_info.message_id].person_id, self.chat_reply_willing[chat.stream_id] ) + current_time = time.time() if chat.stream_id not in self.chat_new_message_time: self.chat_new_message_time[chat.stream_id] = [] - self.chat_new_message_time[chat.stream_id].append(time.time()) + self.chat_new_message_time[chat.stream_id].append(current_time) if len(self.chat_new_message_time[chat.stream_id]) > self.number_of_message_storage: self.chat_new_message_time[chat.stream_id].pop(0) + if chat.stream_id not in self.chat_fatigue_punishment_list: + self.chat_fatigue_punishment_list[chat.stream_id] = [current_time, + self.number_of_message_storage * self.basic_maximum_willing / self.expected_replies_per_min * 60] + def _willing_to_probability(self, willing: float) -> float: """意愿值转化为概率""" willing = max(0, willing) From def7e7a000b12af903e915b9d3adf0e078261c32 Mon Sep 17 00:00:00 2001 From: meng_xi_pan Date: Wed, 16 Apr 2025 11:29:43 +0800 Subject: [PATCH 15/28] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=B8=8A=E6=AC=A1?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BA=A7=E7=94=9F=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/willing/mode_mxp.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/plugins/willing/mode_mxp.py b/src/plugins/willing/mode_mxp.py index f063f6f24..82b5850ae 100644 --- a/src/plugins/willing/mode_mxp.py +++ b/src/plugins/willing/mode_mxp.py @@ -85,9 +85,9 @@ class MxpWillingManager(BaseWillingManager): rel_level = self._get_relationship_level_num(rel_value) self.chat_person_reply_willing[w_info.chat_id][w_info.person_id] += rel_level * 0.05 - now_chat_new_person = self.last_response_person.get(w_info.chat_id, ["", 0]) + now_chat_new_person = self.last_response_person.get(w_info.chat_id, [w_info.person_id, 0]) if now_chat_new_person[0] == w_info.person_id: - if now_chat_new_person[1] < 2: + if now_chat_new_person[1] < 3: now_chat_new_person[1] += 1 else: self.last_response_person[w_info.chat_id] = [w_info.person_id, 0] @@ -101,9 +101,10 @@ class MxpWillingManager(BaseWillingManager): if ( w_info.chat_id in self.last_response_person and self.last_response_person[w_info.chat_id][0] == w_info.person_id + and self.last_response_person[w_info.chat_id][1] ): self.chat_person_reply_willing[w_info.chat_id][w_info.person_id] += self.single_chat_gain * ( - 2 * self.last_response_person[w_info.chat_id][1] + 1 + 2 * self.last_response_person[w_info.chat_id][1] - 1 ) now_chat_new_person = self.last_response_person.get(w_info.chat_id, ["", 0]) if now_chat_new_person[0] != w_info.person_id: @@ -133,16 +134,17 @@ class MxpWillingManager(BaseWillingManager): rel_value = await w_info.person_info_manager.get_value(w_info.person_id, "relationship_value") rel_level = self._get_relationship_level_num(rel_value) current_willing += rel_level * 0.1 - if self.is_debug: + if self.is_debug and rel_level != 0: self.logger.debug(f"关系增益:{rel_level * 0.1}") if ( w_info.chat_id in self.last_response_person and self.last_response_person[w_info.chat_id][0] == w_info.person_id + and self.last_response_person[w_info.chat_id][1] ): current_willing += self.single_chat_gain * (2 * self.last_response_person[w_info.chat_id][1] + 1) if self.is_debug: - self.logger.debug(f"单聊增益:{self.single_chat_gain * (2 * self.last_response_person[w_info.chat_id][1] + 1)}") + self.logger.debug(f"单聊增益:{self.single_chat_gain * (2 * self.last_response_person[w_info.chat_id][1] - 1)}") current_willing += self.chat_fatigue_willing_attenuation.get(w_info.chat_id, 0) if self.is_debug: @@ -222,8 +224,12 @@ class MxpWillingManager(BaseWillingManager): self.chat_new_message_time[chat.stream_id].pop(0) if chat.stream_id not in self.chat_fatigue_punishment_list: - self.chat_fatigue_punishment_list[chat.stream_id] = [current_time, - self.number_of_message_storage * self.basic_maximum_willing / self.expected_replies_per_min * 60] + self.chat_fatigue_punishment_list[chat.stream_id] = [ + (current_time, self.number_of_message_storage * self.basic_maximum_willing / self.expected_replies_per_min * 60) + ] + self.chat_fatigue_willing_attenuation[chat.stream_id] = - 2 * self.basic_maximum_willing + + def _willing_to_probability(self, willing: float) -> float: """意愿值转化为概率""" From cce0c65ff567bf41e0016a8a88c1d8106536ca65 Mon Sep 17 00:00:00 2001 From: meng_xi_pan Date: Wed, 16 Apr 2025 11:38:39 +0800 Subject: [PATCH 16/28] =?UTF-8?q?=E6=BC=8F=E4=BA=86=E4=B8=80=E5=A4=84?= =?UTF-8?q?=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/willing/mode_mxp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/willing/mode_mxp.py b/src/plugins/willing/mode_mxp.py index 82b5850ae..8ed3b60e4 100644 --- a/src/plugins/willing/mode_mxp.py +++ b/src/plugins/willing/mode_mxp.py @@ -227,7 +227,7 @@ class MxpWillingManager(BaseWillingManager): self.chat_fatigue_punishment_list[chat.stream_id] = [ (current_time, self.number_of_message_storage * self.basic_maximum_willing / self.expected_replies_per_min * 60) ] - self.chat_fatigue_willing_attenuation[chat.stream_id] = - 2 * self.basic_maximum_willing + self.chat_fatigue_willing_attenuation[chat.stream_id] = - 2 * self.basic_maximum_willing * self.fatigue_coefficient From 4501e19dc8d4edb06d361f597557e882a44c6d16 Mon Sep 17 00:00:00 2001 From: meng_xi_pan Date: Wed, 16 Apr 2025 14:06:16 +0800 Subject: [PATCH 17/28] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=9B=A0=E4=B8=BA?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E7=B1=BB=E5=9E=8B=E7=BB=93=E6=9E=84=E5=8F=98?= =?UTF-8?q?=E5=8A=A8=E8=80=8C=E5=AF=BC=E8=87=B4buffer=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/message_buffer.py | 24 +++++++++++++++---- .../reasoning_chat/reasoning_chat.py | 14 ++++++++--- .../think_flow_chat/think_flow_chat.py | 14 ++++++++--- 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/plugins/chat/message_buffer.py b/src/plugins/chat/message_buffer.py index 21e490433..c2cfaa826 100644 --- a/src/plugins/chat/message_buffer.py +++ b/src/plugins/chat/message_buffer.py @@ -3,7 +3,7 @@ from src.common.logger import get_module_logger import asyncio from dataclasses import dataclass, field from .message import MessageRecv -from ..message.message_base import BaseMessageInfo, GroupInfo +from ..message.message_base import BaseMessageInfo, GroupInfo, Seg import hashlib from typing import Dict from collections import OrderedDict @@ -130,22 +130,36 @@ class MessageBuffer: keep_msgs = OrderedDict() combined_text = [] found = False - type = "text" + type = "seglist" is_update = True for msg_id, msg in self.buffer_pool[person_id_].items(): if msg_id == message.message_info.message_id: found = True - type = msg.message.message_segment.type + if msg.message.message_segment.type != "seglist": + type = msg.message.message_segment.type + else: + if (isinstance(msg.message.message_segment.data, list) + and all(isinstance(x, Seg) for x in msg.message.message_segment.data) + and len(msg.message.message_segment.data) == 1): + type = msg.message.message_segment.data[0].type combined_text.append(msg.message.processed_plain_text) continue if found: keep_msgs[msg_id] = msg elif msg.result == "F": # 收集F消息的文本内容 + F_type = "seglist" + if msg.message.message_segment.type != "seglist": + F_type = msg.message.message_segment.type + else: + if (isinstance(msg.message.message_segment.data, list) + and all(isinstance(x, Seg) for x in msg.message.message_segment.data) + and len(msg.message.message_segment.data) == 1): + F_type = msg.message.message_segment.data[0].type if hasattr(msg.message, "processed_plain_text") and msg.message.processed_plain_text: - if msg.message.message_segment.type == "text": + if F_type == "text": combined_text.append(msg.message.processed_plain_text) - elif msg.message.message_segment.type != "text": + elif F_type != "text": is_update = False elif msg.result == "U": logger.debug(f"异常未处理信息id: {msg.message.message_info.message_id}") diff --git a/src/plugins/chat_module/reasoning_chat/reasoning_chat.py b/src/plugins/chat_module/reasoning_chat/reasoning_chat.py index 2ce218a6f..acc381f80 100644 --- a/src/plugins/chat_module/reasoning_chat/reasoning_chat.py +++ b/src/plugins/chat_module/reasoning_chat/reasoning_chat.py @@ -192,11 +192,19 @@ class ReasoningChat: if not buffer_result: await willing_manager.bombing_buffer_message_handle(message.message_info.message_id) willing_manager.delete(message.message_info.message_id) - if message.message_segment.type == "text": + F_type = "seglist" + if message.message_segment.type != "seglist": + F_type =message.message_segment.type + else: + if (isinstance(message.message_segment.data, list) + and all(isinstance(x, Seg) for x in message.message_segment.data) + and len(message.message_segment.data) == 1): + F_type = message.message_segment.data[0].type + if F_type == "text": logger.info(f"触发缓冲,已炸飞消息:{message.processed_plain_text}") - elif message.message_segment.type == "image": + elif F_type == "image": logger.info("触发缓冲,已炸飞表情包/图片") - elif message.message_segment.type == "seglist": + elif F_type == "seglist": logger.info("触发缓冲,已炸飞消息列") return diff --git a/src/plugins/chat_module/think_flow_chat/think_flow_chat.py b/src/plugins/chat_module/think_flow_chat/think_flow_chat.py index 1e8e844eb..74d88dd4d 100644 --- a/src/plugins/chat_module/think_flow_chat/think_flow_chat.py +++ b/src/plugins/chat_module/think_flow_chat/think_flow_chat.py @@ -204,11 +204,19 @@ class ThinkFlowChat: if not buffer_result: await willing_manager.bombing_buffer_message_handle(message.message_info.message_id) willing_manager.delete(message.message_info.message_id) - if message.message_segment.type == "text": + F_type = "seglist" + if message.message_segment.type != "seglist": + F_type =message.message_segment.type + else: + if (isinstance(message.message_segment.data, list) + and all(isinstance(x, Seg) for x in message.message_segment.data) + and len(message.message_segment.data) == 1): + F_type = message.message_segment.data[0].type + if F_type == "text": logger.info(f"触发缓冲,已炸飞消息:{message.processed_plain_text}") - elif message.message_segment.type == "image": + elif F_type == "image": logger.info("触发缓冲,已炸飞表情包/图片") - elif message.message_segment.type == "seglist": + elif F_type == "seglist": logger.info("触发缓冲,已炸飞消息列") return From 13dc4562f0f41823bed28017f25cdd6d7af271b3 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 16 Apr 2025 20:22:19 +0800 Subject: [PATCH 18/28] =?UTF-8?q?feat:=E4=BF=AE=E5=A4=8D=E4=B8=AD=E6=96=87?= =?UTF-8?q?=E6=8B=AC=E5=8F=B7=E5=8C=B9=E9=85=8D=EF=BC=8C=E5=8A=A0=E5=85=A5?= =?UTF-8?q?=E5=B0=8F=E5=8A=A9=E6=89=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config_helper/config_helper.py | 477 +++++++++++++++++++++++++++++++++ config_helper/config_notice.md | 10 + src/plugins/chat/utils.py | 6 +- 3 files changed, 492 insertions(+), 1 deletion(-) create mode 100644 config_helper/config_helper.py create mode 100644 config_helper/config_notice.md diff --git a/config_helper/config_helper.py b/config_helper/config_helper.py new file mode 100644 index 000000000..b27aaead4 --- /dev/null +++ b/config_helper/config_helper.py @@ -0,0 +1,477 @@ +import os +import tomli +from packaging.specifiers import SpecifierSet +from packaging.version import Version +import sys + +import asyncio +import os +import time +from typing import Tuple, Union, AsyncGenerator, Generator + +import aiohttp +import requests +import json + + +class EnvInfo: + def __init__(self, env_path): + self.env_path = env_path + self.env_content_txt = None + self.env_content = {} + self.error_message = None + + + def check_env(self): + # 检查根目录是否存在.env文件 + if not os.path.exists(self.env_path): + self.error_message = "根目录没有.env文件,请自己创建或者运行一次MaiBot\n你可以直接复制template/template.env文件到根目录并重命名为.env" + return "not_found" + + #加载整个.env文件 + with open(self.env_path, "r", encoding="utf-8") as f: + self.env_content_txt = f.read() + + #逐行读取所有配置项 + for line in self.env_content_txt.splitlines(): + if line.strip() == "": + continue + key, value = line.split("=", 1) + self.env_content[key.strip()] = value.strip() + + # 检查.env文件的SILICONFLOW_KEY和SILICONFLOW_BASE_URL是否为空 + if "SILICONFLOW_KEY" not in self.env_content or "SILICONFLOW_BASE_URL" not in self.env_content: + if "DEEP_SEEK_BASE_URL" not in self.env_content or "DEEP_SEEK_KEY" not in self.env_content: + self.error_message = "没有设置可用的API和密钥,请检查.env文件,起码配置一个API来让帮助程序工作" + return "not_set" + else: + self.error_message = "你只设置了deepseek官方API,可能无法运行MaiBot,请检查.env文件" + return "only_ds" + + return "success" + +class LLM_request_off: + def __init__(self, model_name="deepseek-ai/DeepSeek-V3", env_info: EnvInfo = None, **kwargs): + self.model_name = model_name + self.params = kwargs + if model_name == "deepseek-ai/DeepSeek-V3" or model_name == "Pro/deepseek-ai/DeepSeek-V3": + self.api_key = env_info.env_content["SILICONFLOW_KEY"] + self.base_url = env_info.env_content["SILICONFLOW_BASE_URL"] + elif model_name == "deepseek-chat": + self.api_key = env_info.env_content["DEEP_SEEK_KEY"] + self.base_url = env_info.env_content["DEEP_SEEK_BASE_URL"] + # logger.info(f"API URL: {self.base_url}") # 使用 logger 记录 base_url + + def generate_stream(self, prompt: str) -> Generator[str, None, None]: + """流式生成模型响应""" + headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"} + + # 构建请求体,启用流式输出 + data = { + "model": self.model_name, + "messages": [{"role": "user", "content": prompt}], + "temperature": 0.4, + "stream": True, + **self.params, + } + + # 发送请求到完整的 chat/completions 端点 + api_url = f"{self.base_url.rstrip('/')}/chat/completions" + print(f"Stream Request URL: {api_url}") + + max_retries = 3 + base_wait_time = 15 + + for retry in range(max_retries): + try: + response = requests.post(api_url, headers=headers, json=data, stream=True) + + if response.status_code == 429: + wait_time = base_wait_time * (2**retry) + print(f"遇到请求限制(429),等待{wait_time}秒后重试...") + time.sleep(wait_time) + continue + + response.raise_for_status() + + # 处理流式响应 + for line in response.iter_lines(): + if line: + line = line.decode('utf-8') + if line.startswith('data: ') and not line.startswith('data: [DONE]'): + json_str = line[6:] # 去掉 "data: " 前缀 + try: + chunk_data = json.loads(json_str) + if ( + "choices" in chunk_data + and len(chunk_data["choices"]) > 0 + and "delta" in chunk_data["choices"][0] + and "content" in chunk_data["choices"][0]["delta"] + ): + content = chunk_data["choices"][0]["delta"]["content"] + yield content + except json.JSONDecodeError: + print(f"无法解析JSON: {json_str}") + return + + except Exception as e: + if retry < max_retries - 1: + wait_time = base_wait_time * (2**retry) + print(f"[流式回复]请求失败,等待{wait_time}秒后重试... 错误: {str(e)}") + time.sleep(wait_time) + else: + print(f"流式请求失败: {str(e)}") + yield f"流式请求失败: {str(e)}" + return + + print("达到最大重试次数,流式请求仍然失败") + yield "达到最大重试次数,流式请求仍然失败" + +class ConfigInfo: + def __init__(self, config_path): + self.config_path = config_path + self.config_content = "" + self.config_content_txt = None + self.template_content = None + self.version = None + self.error_message = None + + def check_bot_config(self): + """ + 检查config/bot_config.toml文件中是否有缺失项目 + """ + + if not os.path.exists(self.config_path): + self.error_message = f"错误:找不到配置文件 {self.config_path}" + return "not found" + + # 读取配置文件,先去掉注释,再解析TOML + try: + # 首先读取原始文件内容 + with open(self.config_path, "r", encoding="utf-8", errors="replace") as f: + file_content = f.read() + + # 去掉注释并保留有效内容 + cleaned_lines = [] + for line in file_content.splitlines(): + # 去掉行内注释 + if "#" in line: + line = line.split("#")[0].rstrip() + + # 如果行不是空的且不是以#开头的注释行,则添加到cleaned_lines + if line.strip() and not line.strip().startswith("#"): + cleaned_lines.append(line) + + # 将处理后的内容用于解析TOML + self.config_content_txt = "\n".join(cleaned_lines) + + # 使用tomli解析处理后的内容 + self.config_content = tomli.loads(self.config_content_txt) + except tomli.TOMLDecodeError as e: + self.error_message = f"错误:配置文件格式错误:{e}" + # 配置内容已经在上面设置了,不需要再次读取 + return "format_error" + except UnicodeDecodeError as e: + self.error_message = f"错误:配置文件编码错误,请使用UTF-8编码:{e}" + return "format_error" + + # 读取模板配置文件 + template_path = "template/bot_config_template.toml" + if not os.path.exists(template_path): + self.error_message = f"错误:找不到模板配置文件,请检查你是否启动过或者该程序是否位于根目录 {template_path}" + return "critical_error" + + try: + with open(template_path, "rb") as f: + template_content = tomli.load(f) + except Exception as e: + self.error_message = f"错误:无法解析模板配置文件,文件损坏,建议重新下载MaiBot:{e}" + return "critical_error" + + # 获取版本信息 + inner_version = self.config_content.get("inner", {}).get("version") + if not inner_version: + self.error_message = "错误:配置文件中缺少版本信息" + return "critical_error" + + try: + self.version = Version(inner_version) + except: + self.error_message = f"错误:版本号格式错误:{inner_version}" + return "critical_error" + + + # 检查所有必需的顶级配置项 + required_sections = [ + "bot", "groups", "personality", "identity", "platforms", + "response", "message", "willing", "emoji", "memory", + "mood", "model" + ] + + missing_sections = [] + for section in required_sections: + if section not in self.config_content: + missing_sections.append(section) + + if missing_sections: + self.error_message = f"错误:配置文件缺少以下顶级配置项:{', '.join(missing_sections)}" + return "critical_error" + + # 检查各个配置项内的必需字段 + missing_fields = [] + + # 检查bot配置 + if "bot" in self.config_content: + bot_config = self.config_content["bot"] + if "qq" not in bot_config: + missing_fields.append("bot.qq") + if "nickname" not in bot_config: + missing_fields.append("bot.nickname") + + # 检查groups配置 + if "groups" in self.config_content: + groups_config = self.config_content["groups"] + if "talk_allowed" not in groups_config: + missing_fields.append("groups.talk_allowed") + + # 检查platforms配置 + if "platforms" in self.config_content: + platforms_config = self.config_content["platforms"] + if not platforms_config or not isinstance(platforms_config, dict) or len(platforms_config) == 0: + missing_fields.append("platforms.(至少一个平台)") + + # 检查模型配置 + if "model" in self.config_content: + model_config = self.config_content["model"] + required_models = [ + "llm_reasoning", "llm_normal", "llm_topic_judge", + "llm_summary_by_topic", "llm_emotion_judge", "embedding", "vlm" + ] + + for model in required_models: + if model not in model_config: + missing_fields.append(f"model.{model}") + elif model in model_config: + model_item = model_config[model] + if "name" not in model_item: + missing_fields.append(f"model.{model}.name") + if "provider" not in model_item: + missing_fields.append(f"model.{model}.provider") + + # 基于模板检查其它必需字段 + def check_section(template_section, user_section, prefix): + if not isinstance(template_section, dict) or not isinstance(user_section, dict): + return + + for key, value in template_section.items(): + # 跳过注释和数组类型的配置项 + if key.startswith("#") or isinstance(value, list): + continue + + if key not in user_section: + missing_fields.append(f"{prefix}.{key}") + elif isinstance(value, dict) and key in user_section: + # 递归检查嵌套配置项 + check_section(value, user_section[key], f"{prefix}.{key}") + + for section in required_sections: + if section in template_content and section in self.config_content: + check_section(template_content[section], self.config_content[section], section) + + # 输出结果 + if missing_fields: + print(f"发现 {len(missing_fields)} 个缺失的配置项:") + for field in missing_fields: + print(f" - {field}") + else: + print("检查完成,没有发现缺失的必要配置项。") + + def get_value(self, path): + """ + 获取配置文件中指定路径的值 + 参数: + path: 以点分隔的路径,例如 "bot.qq" 或 "model.llm_normal.name" + 返回: + 找到的值,如果路径不存在则返回None + """ + if not self.config_content: + return None + + parts = path.split('.') + current = self.config_content + + try: + for part in parts: + if isinstance(current, dict) and part in current: + current = current[part] + else: + return None + return current + except: + return None + + def has_field(self, path): + """ + 检查配置文件中是否存在指定路径 + 参数: + path: 以点分隔的路径,例如 "bot.qq" 或 "model.llm_normal.name" + 返回: + 布尔值,表示路径是否存在 + """ + return self.get_value(path) is not None + + def get_section(self, section_name): + """ + 获取配置文件中的整个部分 + 参数: + section_name: 部分名称,例如 "bot" 或 "model" + 返回: + 字典形式的部分内容,如果部分不存在则返回空字典 + """ + if not self.config_content: + return {} + + return self.config_content.get(section_name, {}) + + def get_all_models(self): + """ + 获取配置中所有的模型配置 + 返回: + 包含所有模型配置的字典 + """ + if not self.config_content or "model" not in self.config_content: + return {} + + return self.config_content.get("model", {}) + + def __str__(self): + return f"配置文件路径:{self.config_path}\n配置文件版本:{self.version}\n错误信息:{self.error_message}" + +class ConfigHelper: + def __init__(self, config_info: ConfigInfo, model_using = "", env_info: EnvInfo = None): + self.config_info = config_info + self.config_notice = None + self.helper_model = LLM_request_off(model_name=model_using,env_info=env_info) + + def deal_format_error(self, error_message, config_content_txt): + prompt = f""" + 这里有一份配置文件存在格式错误,请检查配置文件为什么会出现该错误以及建议如何修改,不要使用markdown格式 + 错误信息:{error_message} + 配置文件内容:{config_content_txt} + 请根据错误信息和配置文件内容,用通俗易懂,简短的语言给出修改建议: + """ + + try: + # 使用流式输出获取分析结果 + print("\n===== 麦麦分析结果 =====") + for chunk in self.helper_model.generate_stream(prompt): + print(chunk, end="", flush=True) + print("\n=====================") + + except Exception as e: + print(f"请求麦麦分析时出错: {str(e)}") + print("请手动检查配置文件格式错误:", error_message) + + def load_config_notice(self): + with open(os.path.join(os.path.dirname(__file__), "config_notice.md"), "r", encoding="utf-8") as f: + self.config_notice = f.read() + + def deal_question(self, question): + prompt = f""" + 这里有一份配置文件,请根据问题给出回答 + 配置文件内容:{self.config_info.config_content_txt} + 关于配置文件的说明:{self.config_notice} + 问题:{question} + """ + + try: + # 使用流式输出获取分析结果 + print("\n===== 麦麦分析结果 =====") + for chunk in self.helper_model.generate_stream(prompt): + print(chunk, end="", flush=True) + print("\n=====================") + + except Exception as e: + print(f"请求麦麦分析时出错: {str(e)}") + + + +if __name__ == "__main__": + model_using = "deepseek-ai/DeepSeek-V3" + # model_using = "Pro/deepseek-ai/DeepSeek-V3" + env_info = EnvInfo(".env") + result = env_info.check_env() + if result == "not_set": + print(env_info.error_message) + exit() + elif result == "only_ds": + model_using = "deepseek-chat" + print("你只设置了deepseek官方API,可能无法运行MaiBot,但是你仍旧可以运行这个帮助程序,请检查.env文件") + elif result == "not_found": + print(env_info.error_message) + exit() + + config_path = "./config/bot_config.toml" + config_info = ConfigInfo(config_path) + print("开始检查config/bot_config.toml文件...") + result = config_info.check_bot_config() + print(config_info) + + helper = ConfigHelper(config_info, model_using, env_info) + helper.load_config_notice() + + # 如果配置文件读取成功,展示如何获取字段 + if config_info.config_content: + print("\n配置文件读取成功,可以访问任意字段:") + # 获取机器人昵称 + nickname = config_info.get_value("bot.nickname") + print(f"机器人昵称: {nickname}") + + # 获取QQ号 + qq = config_info.get_value("bot.qq") + print(f"机器人QQ: {qq}") + + # 获取群聊配置 + groups = config_info.get_section("groups") + print(f"允许聊天的群: {groups.get('talk_allowed', [])}") + + # 获取模型信息 + models = config_info.get_all_models() + print("\n模型配置信息:") + for model_name, model_info in models.items(): + provider = model_info.get("provider", "未知") + model_path = model_info.get("name", "未知") + print(f" - {model_name}: {model_path} (提供商: {provider})") + + # 检查某字段是否存在 + if config_info.has_field("model.llm_normal.temp"): + temp = config_info.get_value("model.llm_normal.temp") + print(f"\n回复模型温度: {temp}") + else: + print("\n回复模型温度未设置") + + # 获取心流相关设置 + if config_info.has_field("heartflow"): + heartflow = config_info.get_section("heartflow") + print(f"\n心流更新间隔: {heartflow.get('heart_flow_update_interval')}秒") + print(f"子心流更新间隔: {heartflow.get('sub_heart_flow_update_interval')}秒") + + if result == "critical_error": + print("配置文件存在严重错误,建议重新下载MaiBot") + exit() + elif result == "format_error": + print("配置文件格式错误,正在进行检查...") + error_message = config_info.error_message + config_content_txt = config_info.config_content_txt + helper.deal_format_error(error_message, config_content_txt) + else: + print("配置文件格式检查完成,没有发现问题") + + while True: + question = input("请输入你遇到的问题,麦麦会帮助你分析(输入exit退出):") + if question == "exit": + break + else: + print("麦麦正在为你分析...") + helper.deal_question(question) + diff --git a/config_helper/config_notice.md b/config_helper/config_notice.md new file mode 100644 index 000000000..b704c8e47 --- /dev/null +++ b/config_helper/config_notice.md @@ -0,0 +1,10 @@ +1.Q:为什么我的bot叫他名字不回消息? +A:请检查qq和nickname字段是否正确填写 +请将默认字段: +qq = 114514 +nickname = "麦麦" +改为你自己的qq号和bot名称(需要与qq昵称相同) + +2. Q:如何修改日程表的内容,或者关闭日程表? +A:日程表目前无法关闭 +如果日程表生成的内容太过科幻或者疯癫,可以尝试调整日程表的温度或者修改日程表描述 \ No newline at end of file diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index dbc12da4e..a6503b807 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -342,13 +342,17 @@ def process_llm_response(text: str) -> List[str]: protected_text, kaomoji_mapping = protect_kaomoji(text) logger.debug(f"保护颜文字后的文本: {protected_text}") # 提取被 () 或 [] 包裹的内容 - pattern = re.compile(r"[\(\[].*?[\)\]]") + pattern = re.compile(r"[\(\[\(].*?[\)\]\)]") # _extracted_contents = pattern.findall(text) _extracted_contents = pattern.findall(protected_text) # 在保护后的文本上查找 # 去除 () 和 [] 及其包裹的内容 # cleaned_text = pattern.sub("", text) cleaned_text = pattern.sub("", protected_text) + + if cleaned_text == '': + return ["呃呃"] + logger.debug(f"{text}去除括号处理后的文本: {cleaned_text}") # 对清理后的文本进行进一步处理 From 3a1d0c623598831a7cca7c9b7658b8901aedc50d Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 16 Apr 2025 20:34:25 +0800 Subject: [PATCH 19/28] =?UTF-8?q?fix=EF=BC=9A=E8=AE=B0=E5=BF=86=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/do_tool/tool_can_use/get_memory.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/do_tool/tool_can_use/get_memory.py b/src/do_tool/tool_can_use/get_memory.py index 6a3c1c391..ae1677006 100644 --- a/src/do_tool/tool_can_use/get_memory.py +++ b/src/do_tool/tool_can_use/get_memory.py @@ -9,7 +9,7 @@ logger = get_module_logger("mid_chat_mem_tool") class GetMemoryTool(BaseTool): """从记忆系统中获取相关记忆的工具""" - name = "mid_chat_mem" + name = "get_memory" description = "从记忆系统中获取相关记忆" parameters = { "type": "object", @@ -49,10 +49,10 @@ class GetMemoryTool(BaseTool): else: content = f"你不太记得有关{text}的记忆,你对此不太了解" - return {"name": "mid_chat_mem", "content": content} + return {"name": "get_memory", "content": content} except Exception as e: logger.error(f"记忆获取工具执行失败: {str(e)}") - return {"name": "mid_chat_mem", "content": f"记忆获取失败: {str(e)}"} + return {"name": "get_memory", "content": f"记忆获取失败: {str(e)}"} # 注册工具 From 8d88bc475c3c32f975d61dc6abfcabf97a5460b2 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 16 Apr 2025 20:43:46 +0800 Subject: [PATCH 20/28] =?UTF-8?q?fix=EF=BC=9A=E7=A7=BB=E9=99=A4=E6=97=A0?= =?UTF-8?q?=E7=94=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/personality_s/big5_test.py | 111 ------- src/plugins/personality_s/can_i_recog_u.py | 353 --------------------- src/plugins/personality_s/combined_test.py | 349 -------------------- src/plugins/personality_s/offline_llm.py | 123 ------- src/plugins/personality_s/questionnaire.py | 142 --------- src/plugins/personality_s/renqingziji.py | 195 ------------ src/plugins/personality_s/who_r_u.py | 156 --------- src/plugins/personality_s/看我.txt | 1 - 8 files changed, 1430 deletions(-) delete mode 100644 src/plugins/personality_s/big5_test.py delete mode 100644 src/plugins/personality_s/can_i_recog_u.py delete mode 100644 src/plugins/personality_s/combined_test.py delete mode 100644 src/plugins/personality_s/offline_llm.py delete mode 100644 src/plugins/personality_s/questionnaire.py delete mode 100644 src/plugins/personality_s/renqingziji.py delete mode 100644 src/plugins/personality_s/who_r_u.py delete mode 100644 src/plugins/personality_s/看我.txt diff --git a/src/plugins/personality_s/big5_test.py b/src/plugins/personality_s/big5_test.py deleted file mode 100644 index a680bce94..000000000 --- a/src/plugins/personality_s/big5_test.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# from .questionnaire import PERSONALITY_QUESTIONS, FACTOR_DESCRIPTIONS - -import os -import sys -from pathlib import Path -import random - -current_dir = Path(__file__).resolve().parent -project_root = current_dir.parent.parent.parent -env_path = project_root / ".env" - -root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) -sys.path.append(root_path) - -from src.plugins.personality.questionnaire import PERSONALITY_QUESTIONS, FACTOR_DESCRIPTIONS # noqa: E402 - - -class BigFiveTest: - def __init__(self): - self.questions = PERSONALITY_QUESTIONS - self.factors = FACTOR_DESCRIPTIONS - - def run_test(self): - """运行测试并收集答案""" - print("\n欢迎参加中国大五人格测试!") - print("\n本测试采用六级评分,请根据每个描述与您的符合程度进行打分:") - print("1 = 完全不符合") - print("2 = 比较不符合") - print("3 = 有点不符合") - print("4 = 有点符合") - print("5 = 比较符合") - print("6 = 完全符合") - print("\n请认真阅读每个描述,选择最符合您实际情况的选项。\n") - - # 创建题目序号到题目的映射 - questions_map = {q["id"]: q for q in self.questions} - - # 获取所有题目ID并随机打乱顺序 - question_ids = list(questions_map.keys()) - random.shuffle(question_ids) - - answers = {} - total_questions = len(question_ids) - - for i, question_id in enumerate(question_ids, 1): - question = questions_map[question_id] - while True: - try: - print(f"\n[{i}/{total_questions}] {question['content']}") - score = int(input("您的评分(1-6): ")) - if 1 <= score <= 6: - answers[question_id] = score - break - else: - print("请输入1-6之间的数字!") - except ValueError: - print("请输入有效的数字!") - - return self.calculate_scores(answers) - - def calculate_scores(self, answers): - """计算各维度得分""" - results = {} - factor_questions = {"外向性": [], "神经质": [], "严谨性": [], "开放性": [], "宜人性": []} - - # 将题目按因子分类 - for q in self.questions: - factor_questions[q["factor"]].append(q) - - # 计算每个维度的得分 - for factor, questions in factor_questions.items(): - total_score = 0 - for q in questions: - score = answers[q["id"]] - # 处理反向计分题目 - if q["reverse_scoring"]: - score = 7 - score # 6分量表反向计分为7减原始分 - total_score += score - - # 计算平均分 - avg_score = round(total_score / len(questions), 2) - results[factor] = {"得分": avg_score, "题目数": len(questions), "总分": total_score} - - return results - - def get_factor_description(self, factor): - """获取因子的详细描述""" - return self.factors[factor] - - -def main(): - test = BigFiveTest() - results = test.run_test() - - print("\n测试结果:") - print("=" * 50) - for factor, data in results.items(): - print(f"\n{factor}:") - print(f"平均分: {data['得分']} (总分: {data['总分']}, 题目数: {data['题目数']})") - print("-" * 30) - description = test.get_factor_description(factor) - print("维度说明:", description["description"][:100] + "...") - print("\n特征词:", ", ".join(description["trait_words"])) - print("=" * 50) - - -if __name__ == "__main__": - main() diff --git a/src/plugins/personality_s/can_i_recog_u.py b/src/plugins/personality_s/can_i_recog_u.py deleted file mode 100644 index c21048e6d..000000000 --- a/src/plugins/personality_s/can_i_recog_u.py +++ /dev/null @@ -1,353 +0,0 @@ -""" -基于聊天记录的人格特征分析系统 -""" - -from typing import Dict, List -import json -import os -from pathlib import Path -from dotenv import load_dotenv -import sys -import random -from collections import defaultdict -import matplotlib.pyplot as plt -import numpy as np -from datetime import datetime -import matplotlib.font_manager as fm - -current_dir = Path(__file__).resolve().parent -project_root = current_dir.parent.parent.parent -env_path = project_root / ".env" - -root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) -sys.path.append(root_path) - -from src.plugins.personality.scene import get_scene_by_factor, PERSONALITY_SCENES # noqa: E402 -from src.plugins.personality.questionnaire import FACTOR_DESCRIPTIONS # noqa: E402 -from src.plugins.personality.offline_llm import LLMModel # noqa: E402 -from src.plugins.personality.who_r_u import MessageAnalyzer # noqa: E402 - -# 加载环境变量 -if env_path.exists(): - print(f"从 {env_path} 加载环境变量") - load_dotenv(env_path) -else: - print(f"未找到环境变量文件: {env_path}") - print("将使用默认配置") - - -class ChatBasedPersonalityEvaluator: - def __init__(self): - self.personality_traits = {"开放性": 0, "严谨性": 0, "外向性": 0, "宜人性": 0, "神经质": 0} - self.scenarios = [] - self.message_analyzer = MessageAnalyzer() - self.llm = LLMModel() - self.trait_scores_history = defaultdict(list) # 记录每个特质的得分历史 - - # 为每个人格特质获取对应的场景 - for trait in PERSONALITY_SCENES: - scenes = get_scene_by_factor(trait) - if not scenes: - continue - scene_keys = list(scenes.keys()) - selected_scenes = random.sample(scene_keys, min(3, len(scene_keys))) - - for scene_key in selected_scenes: - scene = scenes[scene_key] - other_traits = [t for t in PERSONALITY_SCENES if t != trait] - secondary_trait = random.choice(other_traits) - self.scenarios.append( - {"场景": scene["scenario"], "评估维度": [trait, secondary_trait], "场景编号": scene_key} - ) - - def analyze_chat_context(self, messages: List[Dict]) -> str: - """ - 分析一组消息的上下文,生成场景描述 - """ - context = "" - for msg in messages: - nickname = msg.get("user_info", {}).get("user_nickname", "未知用户") - content = msg.get("processed_plain_text", msg.get("detailed_plain_text", "")) - if content: - context += f"{nickname}: {content}\n" - return context - - def evaluate_chat_response( - self, user_nickname: str, chat_context: str, dimensions: List[str] = None - ) -> Dict[str, float]: - """ - 评估聊天内容在各个人格维度上的得分 - """ - # 使用所有维度进行评估 - dimensions = list(self.personality_traits.keys()) - - dimension_descriptions = [] - for dim in dimensions: - desc = FACTOR_DESCRIPTIONS.get(dim, "") - if desc: - dimension_descriptions.append(f"- {dim}:{desc}") - - dimensions_text = "\n".join(dimension_descriptions) - - prompt = f"""请根据以下聊天记录,评估"{user_nickname}"在大五人格模型中的维度得分(1-6分)。 - -聊天记录: -{chat_context} - -需要评估的维度说明: -{dimensions_text} - -请按照以下格式输出评估结果,注意,你的评价对象是"{user_nickname}"(仅输出JSON格式): -{{ - "开放性": 分数, - "严谨性": 分数, - "外向性": 分数, - "宜人性": 分数, - "神经质": 分数 -}} - -评分标准: -1 = 非常不符合该维度特征 -2 = 比较不符合该维度特征 -3 = 有点不符合该维度特征 -4 = 有点符合该维度特征 -5 = 比较符合该维度特征 -6 = 非常符合该维度特征 - -如果你觉得某个维度没有相关信息或者无法判断,请输出0分 - -请根据聊天记录的内容和语气,结合维度说明进行评分。如果维度可以评分,确保分数在1-6之间。如果没有体现,请输出0分""" - - try: - ai_response, _ = self.llm.generate_response(prompt) - start_idx = ai_response.find("{") - end_idx = ai_response.rfind("}") + 1 - if start_idx != -1 and end_idx != 0: - json_str = ai_response[start_idx:end_idx] - scores = json.loads(json_str) - return {k: max(0, min(6, float(v))) for k, v in scores.items()} - else: - print("AI响应格式不正确,使用默认评分") - return {dim: 0 for dim in dimensions} - except Exception as e: - print(f"评估过程出错:{str(e)}") - return {dim: 0 for dim in dimensions} - - def evaluate_user_personality(self, qq_id: str, num_samples: int = 10, context_length: int = 5) -> Dict: - """ - 基于用户的聊天记录评估人格特征 - - Args: - qq_id (str): 用户QQ号 - num_samples (int): 要分析的聊天片段数量 - context_length (int): 每个聊天片段的上下文长度 - - Returns: - Dict: 评估结果 - """ - # 获取用户的随机消息及其上下文 - chat_contexts, user_nickname = self.message_analyzer.get_user_random_contexts( - qq_id, num_messages=num_samples, context_length=context_length - ) - if not chat_contexts: - return {"error": f"没有找到QQ号 {qq_id} 的消息记录"} - - # 初始化评分 - final_scores = defaultdict(float) - dimension_counts = defaultdict(int) - chat_samples = [] - - # 清空历史记录 - self.trait_scores_history.clear() - - # 分析每个聊天上下文 - for chat_context in chat_contexts: - # 评估这段聊天内容的所有维度 - scores = self.evaluate_chat_response(user_nickname, chat_context) - - # 记录样本 - chat_samples.append( - {"聊天内容": chat_context, "评估维度": list(self.personality_traits.keys()), "评分": scores} - ) - - # 更新总分和历史记录 - for dimension, score in scores.items(): - if score > 0: # 只统计大于0的有效分数 - final_scores[dimension] += score - dimension_counts[dimension] += 1 - self.trait_scores_history[dimension].append(score) - - # 计算平均分 - average_scores = {} - for dimension in self.personality_traits: - if dimension_counts[dimension] > 0: - average_scores[dimension] = round(final_scores[dimension] / dimension_counts[dimension], 2) - else: - average_scores[dimension] = 0 # 如果没有有效分数,返回0 - - # 生成趋势图 - self._generate_trend_plot(qq_id, user_nickname) - - result = { - "用户QQ": qq_id, - "用户昵称": user_nickname, - "样本数量": len(chat_samples), - "人格特征评分": average_scores, - "维度评估次数": dict(dimension_counts), - "详细样本": chat_samples, - "特质得分历史": {k: v for k, v in self.trait_scores_history.items()}, - } - - # 保存结果 - os.makedirs("results", exist_ok=True) - result_file = f"results/personality_result_{qq_id}.json" - with open(result_file, "w", encoding="utf-8") as f: - json.dump(result, f, ensure_ascii=False, indent=2) - - return result - - def _generate_trend_plot(self, qq_id: str, user_nickname: str): - """ - 生成人格特质累计平均分变化趋势图 - """ - # 查找系统中可用的中文字体 - chinese_fonts = [] - for f in fm.fontManager.ttflist: - try: - if "简" in f.name or "SC" in f.name or "黑" in f.name or "宋" in f.name or "微软" in f.name: - chinese_fonts.append(f.name) - except Exception: - continue - - if chinese_fonts: - plt.rcParams["font.sans-serif"] = chinese_fonts + ["SimHei", "Microsoft YaHei", "Arial Unicode MS"] - else: - # 如果没有找到中文字体,使用默认字体,并将中文昵称转换为拼音或英文 - try: - from pypinyin import lazy_pinyin - - user_nickname = "".join(lazy_pinyin(user_nickname)) - except ImportError: - user_nickname = "User" # 如果无法转换为拼音,使用默认英文 - - plt.rcParams["axes.unicode_minus"] = False # 解决负号显示问题 - - plt.figure(figsize=(12, 6)) - plt.style.use("bmh") # 使用内置的bmh样式,它有类似seaborn的美观效果 - - colors = { - "开放性": "#FF9999", - "严谨性": "#66B2FF", - "外向性": "#99FF99", - "宜人性": "#FFCC99", - "神经质": "#FF99CC", - } - - # 计算每个维度在每个时间点的累计平均分 - cumulative_averages = {} - for trait, scores in self.trait_scores_history.items(): - if not scores: - continue - - averages = [] - total = 0 - valid_count = 0 - for score in scores: - if score > 0: # 只计算大于0的有效分数 - total += score - valid_count += 1 - if valid_count > 0: - averages.append(total / valid_count) - else: - # 如果当前分数无效,使用前一个有效的平均分 - if averages: - averages.append(averages[-1]) - else: - continue # 跳过无效分数 - - if averages: # 只有在有有效分数的情况下才添加到累计平均中 - cumulative_averages[trait] = averages - - # 绘制每个维度的累计平均分变化趋势 - for trait, averages in cumulative_averages.items(): - x = range(1, len(averages) + 1) - plt.plot(x, averages, "o-", label=trait, color=colors.get(trait), linewidth=2, markersize=8) - - # 添加趋势线 - z = np.polyfit(x, averages, 1) - p = np.poly1d(z) - plt.plot(x, p(x), "--", color=colors.get(trait), alpha=0.5) - - plt.title(f"{user_nickname} 的人格特质累计平均分变化趋势", fontsize=14, pad=20) - plt.xlabel("评估次数", fontsize=12) - plt.ylabel("累计平均分", fontsize=12) - plt.grid(True, linestyle="--", alpha=0.7) - plt.legend(loc="center left", bbox_to_anchor=(1, 0.5)) - plt.ylim(0, 7) - plt.tight_layout() - - # 保存图表 - os.makedirs("results/plots", exist_ok=True) - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - plot_file = f"results/plots/personality_trend_{qq_id}_{timestamp}.png" - plt.savefig(plot_file, dpi=300, bbox_inches="tight") - plt.close() - - -def analyze_user_personality(qq_id: str, num_samples: int = 10, context_length: int = 5) -> str: - """ - 分析用户人格特征的便捷函数 - - Args: - qq_id (str): 用户QQ号 - num_samples (int): 要分析的聊天片段数量 - context_length (int): 每个聊天片段的上下文长度 - - Returns: - str: 格式化的分析结果 - """ - evaluator = ChatBasedPersonalityEvaluator() - result = evaluator.evaluate_user_personality(qq_id, num_samples, context_length) - - if "error" in result: - return result["error"] - - # 格式化输出 - output = f"QQ号 {qq_id} ({result['用户昵称']}) 的人格特征分析结果:\n" - output += "=" * 50 + "\n\n" - - output += "人格特征评分:\n" - for trait, score in result["人格特征评分"].items(): - if score == 0: - output += f"{trait}: 数据不足,无法判断 (评估次数: {result['维度评估次数'].get(trait, 0)})\n" - else: - output += f"{trait}: {score}/6 (评估次数: {result['维度评估次数'].get(trait, 0)})\n" - - # 添加变化趋势描述 - if trait in result["特质得分历史"] and len(result["特质得分历史"][trait]) > 1: - scores = [s for s in result["特质得分历史"][trait] if s != 0] # 过滤掉无效分数 - if len(scores) > 1: # 确保有足够的有效分数计算趋势 - trend = np.polyfit(range(len(scores)), scores, 1)[0] - if abs(trend) < 0.1: - trend_desc = "保持稳定" - elif trend > 0: - trend_desc = "呈上升趋势" - else: - trend_desc = "呈下降趋势" - output += f" 变化趋势: {trend_desc} (斜率: {trend:.2f})\n" - - output += f"\n分析样本数量:{result['样本数量']}\n" - output += f"结果已保存至:results/personality_result_{qq_id}.json\n" - output += "变化趋势图已保存至:results/plots/目录\n" - - return output - - -if __name__ == "__main__": - # 测试代码 - # test_qq = "" # 替换为要测试的QQ号 - # print(analyze_user_personality(test_qq, num_samples=30, context_length=20)) - # test_qq = "" - # print(analyze_user_personality(test_qq, num_samples=30, context_length=20)) - test_qq = "1026294844" - print(analyze_user_personality(test_qq, num_samples=30, context_length=30)) diff --git a/src/plugins/personality_s/combined_test.py b/src/plugins/personality_s/combined_test.py deleted file mode 100644 index 1a1e9060e..000000000 --- a/src/plugins/personality_s/combined_test.py +++ /dev/null @@ -1,349 +0,0 @@ -from typing import Dict -import json -import os -from pathlib import Path -import sys -from datetime import datetime -import random -from scipy import stats # 添加scipy导入用于t检验 - -current_dir = Path(__file__).resolve().parent -project_root = current_dir.parent.parent.parent -env_path = project_root / ".env" - -root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) -sys.path.append(root_path) - -from src.plugins.personality.big5_test import BigFiveTest # noqa: E402 -from src.plugins.personality.renqingziji import PersonalityEvaluator_direct # noqa: E402 -from src.plugins.personality.questionnaire import FACTOR_DESCRIPTIONS, PERSONALITY_QUESTIONS # noqa: E402 - - -class CombinedPersonalityTest: - def __init__(self): - self.big5_test = BigFiveTest() - self.scenario_test = PersonalityEvaluator_direct() - self.dimensions = ["开放性", "严谨性", "外向性", "宜人性", "神经质"] - - def run_combined_test(self): - """运行组合测试""" - print("\n=== 人格特征综合评估系统 ===") - print("\n本测试将通过两种方式评估人格特征:") - print("1. 传统问卷测评(约40题)") - print("2. 情景反应测评(15个场景)") - print("\n两种测评完成后,将对比分析结果的异同。") - input("\n准备好开始第一部分(问卷测评)了吗?按回车继续...") - - # 运行问卷测试 - print("\n=== 第一部分:问卷测评 ===") - print("本部分采用六级评分,请根据每个描述与您的符合程度进行打分:") - print("1 = 完全不符合") - print("2 = 比较不符合") - print("3 = 有点不符合") - print("4 = 有点符合") - print("5 = 比较符合") - print("6 = 完全符合") - print("\n重要提示:您可以选择以下两种方式之一来回答问题:") - print("1. 根据您自身的真实情况来回答") - print("2. 根据您想要扮演的角色特征来回答") - print("\n无论选择哪种方式,请保持一致并认真回答每个问题。") - input("\n按回车开始答题...") - - questionnaire_results = self.run_questionnaire() - - # 转换问卷结果格式以便比较 - questionnaire_scores = {factor: data["得分"] for factor, data in questionnaire_results.items()} - - # 运行情景测试 - print("\n=== 第二部分:情景反应测评 ===") - print("接下来,您将面对一系列具体场景,请描述您在每个场景中可能的反应。") - print("每个场景都会评估不同的人格维度,共15个场景。") - print("您可以选择提供自己的真实反应,也可以选择扮演一个您创作的角色来回答。") - input("\n准备好开始了吗?按回车继续...") - - scenario_results = self.run_scenario_test() - - # 比较和展示结果 - self.compare_and_display_results(questionnaire_scores, scenario_results) - - # 保存结果 - self.save_results(questionnaire_scores, scenario_results) - - def run_questionnaire(self): - """运行问卷测试部分""" - # 创建题目序号到题目的映射 - questions_map = {q["id"]: q for q in PERSONALITY_QUESTIONS} - - # 获取所有题目ID并随机打乱顺序 - question_ids = list(questions_map.keys()) - random.shuffle(question_ids) - - answers = {} - total_questions = len(question_ids) - - for i, question_id in enumerate(question_ids, 1): - question = questions_map[question_id] - while True: - try: - print(f"\n问题 [{i}/{total_questions}]") - print(f"{question['content']}") - score = int(input("您的评分(1-6): ")) - if 1 <= score <= 6: - answers[question_id] = score - break - else: - print("请输入1-6之间的数字!") - except ValueError: - print("请输入有效的数字!") - - # 每10题显示一次进度 - if i % 10 == 0: - print(f"\n已完成 {i}/{total_questions} 题 ({int(i / total_questions * 100)}%)") - - return self.calculate_questionnaire_scores(answers) - - def calculate_questionnaire_scores(self, answers): - """计算问卷测试的维度得分""" - results = {} - factor_questions = {"外向性": [], "神经质": [], "严谨性": [], "开放性": [], "宜人性": []} - - # 将题目按因子分类 - for q in PERSONALITY_QUESTIONS: - factor_questions[q["factor"]].append(q) - - # 计算每个维度的得分 - for factor, questions in factor_questions.items(): - total_score = 0 - for q in questions: - score = answers[q["id"]] - # 处理反向计分题目 - if q["reverse_scoring"]: - score = 7 - score # 6分量表反向计分为7减原始分 - total_score += score - - # 计算平均分 - avg_score = round(total_score / len(questions), 2) - results[factor] = {"得分": avg_score, "题目数": len(questions), "总分": total_score} - - return results - - def run_scenario_test(self): - """运行情景测试部分""" - final_scores = {"开放性": 0, "严谨性": 0, "外向性": 0, "宜人性": 0, "神经质": 0} - dimension_counts = {trait: 0 for trait in final_scores.keys()} - - # 随机打乱场景顺序 - scenarios = self.scenario_test.scenarios.copy() - random.shuffle(scenarios) - - for i, scenario_data in enumerate(scenarios, 1): - print(f"\n场景 [{i}/{len(scenarios)}] - {scenario_data['场景编号']}") - print("-" * 50) - print(scenario_data["场景"]) - print("\n请描述您在这种情况下会如何反应:") - response = input().strip() - - if not response: - print("反应描述不能为空!") - continue - - print("\n正在评估您的描述...") - scores = self.scenario_test.evaluate_response(scenario_data["场景"], response, scenario_data["评估维度"]) - - # 更新分数 - for dimension, score in scores.items(): - final_scores[dimension] += score - dimension_counts[dimension] += 1 - - # print("\n当前场景评估结果:") - # print("-" * 30) - # for dimension, score in scores.items(): - # print(f"{dimension}: {score}/6") - - # 每5个场景显示一次总进度 - if i % 5 == 0: - print(f"\n已完成 {i}/{len(scenarios)} 个场景 ({int(i / len(scenarios) * 100)}%)") - - if i < len(scenarios): - input("\n按回车继续下一个场景...") - - # 计算平均分 - for dimension in final_scores: - if dimension_counts[dimension] > 0: - final_scores[dimension] = round(final_scores[dimension] / dimension_counts[dimension], 2) - - return final_scores - - def compare_and_display_results(self, questionnaire_scores: Dict, scenario_scores: Dict): - """比较和展示两种测试的结果""" - print("\n=== 测评结果对比分析 ===") - print("\n" + "=" * 60) - print(f"{'维度':<8} {'问卷得分':>10} {'情景得分':>10} {'差异':>10} {'差异程度':>10}") - print("-" * 60) - - # 收集每个维度的得分用于统计分析 - questionnaire_values = [] - scenario_values = [] - diffs = [] - - for dimension in self.dimensions: - q_score = questionnaire_scores[dimension] - s_score = scenario_scores[dimension] - diff = round(abs(q_score - s_score), 2) - - questionnaire_values.append(q_score) - scenario_values.append(s_score) - diffs.append(diff) - - # 计算差异程度 - diff_level = "低" if diff < 0.5 else "中" if diff < 1.0 else "高" - print(f"{dimension:<8} {q_score:>10.2f} {s_score:>10.2f} {diff:>10.2f} {diff_level:>10}") - - print("=" * 60) - - # 计算整体统计指标 - mean_diff = sum(diffs) / len(diffs) - std_diff = (sum((x - mean_diff) ** 2 for x in diffs) / (len(diffs) - 1)) ** 0.5 - - # 计算效应量 (Cohen's d) - pooled_std = ( - ( - sum((x - sum(questionnaire_values) / len(questionnaire_values)) ** 2 for x in questionnaire_values) - + sum((x - sum(scenario_values) / len(scenario_values)) ** 2 for x in scenario_values) - ) - / (2 * len(self.dimensions) - 2) - ) ** 0.5 - - if pooled_std != 0: - cohens_d = abs(mean_diff / pooled_std) - - # 解释效应量 - if cohens_d < 0.2: - effect_size = "微小" - elif cohens_d < 0.5: - effect_size = "小" - elif cohens_d < 0.8: - effect_size = "中等" - else: - effect_size = "大" - - # 对所有维度进行整体t检验 - t_stat, p_value = stats.ttest_rel(questionnaire_values, scenario_values) - print("\n整体统计分析:") - print(f"平均差异: {mean_diff:.3f}") - print(f"差异标准差: {std_diff:.3f}") - print(f"效应量(Cohen's d): {cohens_d:.3f}") - print(f"效应量大小: {effect_size}") - print(f"t统计量: {t_stat:.3f}") - print(f"p值: {p_value:.3f}") - - if p_value < 0.05: - print("结论: 两种测评方法的结果存在显著差异 (p < 0.05)") - else: - print("结论: 两种测评方法的结果无显著差异 (p >= 0.05)") - - print("\n维度说明:") - for dimension in self.dimensions: - print(f"\n{dimension}:") - desc = FACTOR_DESCRIPTIONS[dimension] - print(f"定义:{desc['description']}") - print(f"特征词:{', '.join(desc['trait_words'])}") - - # 分析显著差异 - significant_diffs = [] - for dimension in self.dimensions: - diff = abs(questionnaire_scores[dimension] - scenario_scores[dimension]) - if diff >= 1.0: # 差异大于等于1分视为显著 - significant_diffs.append( - { - "dimension": dimension, - "diff": diff, - "questionnaire": questionnaire_scores[dimension], - "scenario": scenario_scores[dimension], - } - ) - - if significant_diffs: - print("\n\n显著差异分析:") - print("-" * 40) - for diff in significant_diffs: - print(f"\n{diff['dimension']}维度的测评结果存在显著差异:") - print(f"问卷得分:{diff['questionnaire']:.2f}") - print(f"情景得分:{diff['scenario']:.2f}") - print(f"差异值:{diff['diff']:.2f}") - - # 分析可能的原因 - if diff["questionnaire"] > diff["scenario"]: - print("可能原因:在问卷中的自我评价较高,但在具体情景中的表现较为保守。") - else: - print("可能原因:在具体情景中表现出更多该维度特征,而在问卷自评时较为保守。") - - def save_results(self, questionnaire_scores: Dict, scenario_scores: Dict): - """保存测试结果""" - results = { - "测试时间": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "问卷测评结果": questionnaire_scores, - "情景测评结果": scenario_scores, - "维度说明": FACTOR_DESCRIPTIONS, - } - - # 确保目录存在 - os.makedirs("results", exist_ok=True) - - # 生成带时间戳的文件名 - filename = f"results/personality_combined_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" - - # 保存到文件 - with open(filename, "w", encoding="utf-8") as f: - json.dump(results, f, ensure_ascii=False, indent=2) - - print(f"\n完整的测评结果已保存到:{filename}") - - -def load_existing_results(): - """检查并加载已有的测试结果""" - results_dir = "results" - if not os.path.exists(results_dir): - return None - - # 获取所有personality_combined开头的文件 - result_files = [f for f in os.listdir(results_dir) if f.startswith("personality_combined_") and f.endswith(".json")] - - if not result_files: - return None - - # 按文件修改时间排序,获取最新的结果文件 - latest_file = max(result_files, key=lambda f: os.path.getmtime(os.path.join(results_dir, f))) - - print(f"\n发现已有的测试结果:{latest_file}") - try: - with open(os.path.join(results_dir, latest_file), "r", encoding="utf-8") as f: - results = json.load(f) - return results - except Exception as e: - print(f"读取结果文件时出错:{str(e)}") - return None - - -def main(): - test = CombinedPersonalityTest() - - # 检查是否存在已有结果 - existing_results = load_existing_results() - - if existing_results: - print("\n=== 使用已有测试结果进行分析 ===") - print(f"测试时间:{existing_results['测试时间']}") - - questionnaire_scores = existing_results["问卷测评结果"] - scenario_scores = existing_results["情景测评结果"] - - # 直接进行结果对比分析 - test.compare_and_display_results(questionnaire_scores, scenario_scores) - else: - print("\n未找到已有的测试结果,开始新的测试...") - test.run_combined_test() - - -if __name__ == "__main__": - main() diff --git a/src/plugins/personality_s/offline_llm.py b/src/plugins/personality_s/offline_llm.py deleted file mode 100644 index db51ca00f..000000000 --- a/src/plugins/personality_s/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 "达到最大重试次数,请求仍然失败", "" diff --git a/src/plugins/personality_s/questionnaire.py b/src/plugins/personality_s/questionnaire.py deleted file mode 100644 index 8e965061d..000000000 --- a/src/plugins/personality_s/questionnaire.py +++ /dev/null @@ -1,142 +0,0 @@ -# 人格测试问卷题目 -# 王孟成, 戴晓阳, & 姚树桥. (2011). -# 中国大五人格问卷的初步编制Ⅲ:简式版的制定及信效度检验. 中国临床心理学杂志, 19(04), Article 04. - -# 王孟成, 戴晓阳, & 姚树桥. (2010). -# 中国大五人格问卷的初步编制Ⅰ:理论框架与信度分析. 中国临床心理学杂志, 18(05), Article 05. - -PERSONALITY_QUESTIONS = [ - # 神经质维度 (F1) - {"id": 1, "content": "我常担心有什么不好的事情要发生", "factor": "神经质", "reverse_scoring": False}, - {"id": 2, "content": "我常感到害怕", "factor": "神经质", "reverse_scoring": False}, - {"id": 3, "content": "有时我觉得自己一无是处", "factor": "神经质", "reverse_scoring": False}, - {"id": 4, "content": "我很少感到忧郁或沮丧", "factor": "神经质", "reverse_scoring": True}, - {"id": 5, "content": "别人一句漫不经心的话,我常会联系在自己身上", "factor": "神经质", "reverse_scoring": False}, - {"id": 6, "content": "在面对压力时,我有种快要崩溃的感觉", "factor": "神经质", "reverse_scoring": False}, - {"id": 7, "content": "我常担忧一些无关紧要的事情", "factor": "神经质", "reverse_scoring": False}, - {"id": 8, "content": "我常常感到内心不踏实", "factor": "神经质", "reverse_scoring": False}, - # 严谨性维度 (F2) - {"id": 9, "content": "在工作上,我常只求能应付过去便可", "factor": "严谨性", "reverse_scoring": True}, - {"id": 10, "content": "一旦确定了目标,我会坚持努力地实现它", "factor": "严谨性", "reverse_scoring": False}, - {"id": 11, "content": "我常常是仔细考虑之后才做出决定", "factor": "严谨性", "reverse_scoring": False}, - {"id": 12, "content": "别人认为我是个慎重的人", "factor": "严谨性", "reverse_scoring": False}, - {"id": 13, "content": "做事讲究逻辑和条理是我的一个特点", "factor": "严谨性", "reverse_scoring": False}, - {"id": 14, "content": "我喜欢一开头就把事情计划好", "factor": "严谨性", "reverse_scoring": False}, - {"id": 15, "content": "我工作或学习很勤奋", "factor": "严谨性", "reverse_scoring": False}, - {"id": 16, "content": "我是个倾尽全力做事的人", "factor": "严谨性", "reverse_scoring": False}, - # 宜人性维度 (F3) - { - "id": 17, - "content": "尽管人类社会存在着一些阴暗的东西(如战争、罪恶、欺诈),我仍然相信人性总的来说是善良的", - "factor": "宜人性", - "reverse_scoring": False, - }, - {"id": 18, "content": "我觉得大部分人基本上是心怀善意的", "factor": "宜人性", "reverse_scoring": False}, - {"id": 19, "content": "虽然社会上有骗子,但我觉得大部分人还是可信的", "factor": "宜人性", "reverse_scoring": False}, - {"id": 20, "content": "我不太关心别人是否受到不公正的待遇", "factor": "宜人性", "reverse_scoring": True}, - {"id": 21, "content": "我时常觉得别人的痛苦与我无关", "factor": "宜人性", "reverse_scoring": True}, - {"id": 22, "content": "我常为那些遭遇不幸的人感到难过", "factor": "宜人性", "reverse_scoring": False}, - {"id": 23, "content": "我是那种只照顾好自己,不替别人担忧的人", "factor": "宜人性", "reverse_scoring": True}, - {"id": 24, "content": "当别人向我诉说不幸时,我常感到难过", "factor": "宜人性", "reverse_scoring": False}, - # 开放性维度 (F4) - {"id": 25, "content": "我的想象力相当丰富", "factor": "开放性", "reverse_scoring": False}, - {"id": 26, "content": "我头脑中经常充满生动的画面", "factor": "开放性", "reverse_scoring": False}, - {"id": 27, "content": "我对许多事情有着很强的好奇心", "factor": "开放性", "reverse_scoring": False}, - {"id": 28, "content": "我喜欢冒险", "factor": "开放性", "reverse_scoring": False}, - {"id": 29, "content": "我是个勇于冒险,突破常规的人", "factor": "开放性", "reverse_scoring": False}, - {"id": 30, "content": "我身上具有别人没有的冒险精神", "factor": "开放性", "reverse_scoring": False}, - { - "id": 31, - "content": "我渴望学习一些新东西,即使它们与我的日常生活无关", - "factor": "开放性", - "reverse_scoring": False, - }, - { - "id": 32, - "content": "我很愿意也很容易接受那些新事物、新观点、新想法", - "factor": "开放性", - "reverse_scoring": False, - }, - # 外向性维度 (F5) - {"id": 33, "content": "我喜欢参加社交与娱乐聚会", "factor": "外向性", "reverse_scoring": False}, - {"id": 34, "content": "我对人多的聚会感到乏味", "factor": "外向性", "reverse_scoring": True}, - {"id": 35, "content": "我尽量避免参加人多的聚会和嘈杂的环境", "factor": "外向性", "reverse_scoring": True}, - {"id": 36, "content": "在热闹的聚会上,我常常表现主动并尽情玩耍", "factor": "外向性", "reverse_scoring": False}, - {"id": 37, "content": "有我在的场合一般不会冷场", "factor": "外向性", "reverse_scoring": False}, - {"id": 38, "content": "我希望成为领导者而不是被领导者", "factor": "外向性", "reverse_scoring": False}, - {"id": 39, "content": "在一个团体中,我希望处于领导地位", "factor": "外向性", "reverse_scoring": False}, - {"id": 40, "content": "别人多认为我是一个热情和友好的人", "factor": "外向性", "reverse_scoring": False}, -] - -# 因子维度说明 -FACTOR_DESCRIPTIONS = { - "外向性": { - "description": "反映个体神经系统的强弱和动力特征。外向性主要表现为个体在人际交往和社交活动中的倾向性," - "包括对社交活动的兴趣、" - "对人群的态度、社交互动中的主动程度以及在群体中的影响力。高分者倾向于积极参与社交活动,乐于与人交往,善于表达自我," - "并往往在群体中发挥领导作用;低分者则倾向于独处,不喜欢热闹的社交场合,表现出内向、安静的特征。", - "trait_words": ["热情", "活力", "社交", "主动"], - "subfactors": { - "合群性": "个体愿意与他人聚在一起,即接近人群的倾向;高分表现乐群、好交际,低分表现封闭、独处", - "热情": "个体对待别人时所表现出的态度;高分表现热情好客,低分表现冷淡", - "支配性": "个体喜欢指使、操纵他人,倾向于领导别人的特点;高分表现好强、发号施令,低分表现顺从、低调", - "活跃": "个体精力充沛,活跃、主动性等特点;高分表现活跃,低分表现安静", - }, - }, - "神经质": { - "description": "反映个体情绪的状态和体验内心苦恼的倾向性。这个维度主要关注个体在面对压力、" - "挫折和日常生活挑战时的情绪稳定性和适应能力。它包含了对焦虑、抑郁、愤怒等负面情绪的敏感程度," - "以及个体对这些情绪的调节和控制能力。高分者容易体验负面情绪,对压力较为敏感,情绪波动较大;" - "低分者则表现出较强的情绪稳定性,能够较好地应对压力和挫折。", - "trait_words": ["稳定", "沉着", "从容", "坚韧"], - "subfactors": { - "焦虑": "个体体验焦虑感的个体差异;高分表现坐立不安,低分表现平静", - "抑郁": "个体体验抑郁情感的个体差异;高分表现郁郁寡欢,低分表现平静", - "敏感多疑": "个体常常关注自己的内心活动,行为和过于意识人对自己的看法、评价;高分表现敏感多疑," - "低分表现淡定、自信", - "脆弱性": "个体在危机或困难面前无力、脆弱的特点;高分表现无能、易受伤、逃避,低分表现坚强", - "愤怒-敌意": "个体准备体验愤怒,及相关情绪的状态;高分表现暴躁易怒,低分表现平静", - }, - }, - "严谨性": { - "description": "反映个体在目标导向行为上的组织、坚持和动机特征。这个维度体现了个体在工作、" - "学习等目标性活动中的自我约束和行为管理能力。它涉及到个体的责任感、自律性、计划性、条理性以及完成任务的态度。" - "高分者往往表现出强烈的责任心、良好的组织能力、谨慎的决策风格和持续的努力精神;低分者则可能表现出随意性强、" - "缺乏规划、做事马虎或易放弃的特点。", - "trait_words": ["负责", "自律", "条理", "勤奋"], - "subfactors": { - "责任心": "个体对待任务和他人认真负责,以及对自己承诺的信守;高分表现有责任心、负责任," - "低分表现推卸责任、逃避处罚", - "自我控制": "个体约束自己的能力,及自始至终的坚持性;高分表现自制、有毅力,低分表现冲动、无毅力", - "审慎性": "个体在采取具体行动前的心理状态;高分表现谨慎、小心,低分表现鲁莽、草率", - "条理性": "个体处理事务和工作的秩序,条理和逻辑性;高分表现整洁、有秩序,低分表现混乱、遗漏", - "勤奋": "个体工作和学习的努力程度及为达到目标而表现出的进取精神;高分表现勤奋、刻苦,低分表现懒散", - }, - }, - "开放性": { - "description": "反映个体对新异事物、新观念和新经验的接受程度,以及在思维和行为方面的创新倾向。" - "这个维度体现了个体在认知和体验方面的广度、深度和灵活性。它包括对艺术的欣赏能力、对知识的求知欲、想象力的丰富程度," - "以及对冒险和创新的态度。高分者往往具有丰富的想象力、广泛的兴趣、开放的思维方式和创新的倾向;低分者则倾向于保守、" - "传统,喜欢熟悉和常规的事物。", - "trait_words": ["创新", "好奇", "艺术", "冒险"], - "subfactors": { - "幻想": "个体富于幻想和想象的水平;高分表现想象力丰富,低分表现想象力匮乏", - "审美": "个体对于艺术和美的敏感与热爱程度;高分表现富有艺术气息,低分表现一般对艺术不敏感", - "好奇心": "个体对未知事物的态度;高分表现兴趣广泛、好奇心浓,低分表现兴趣少、无好奇心", - "冒险精神": "个体愿意尝试有风险活动的个体差异;高分表现好冒险,低分表现保守", - "价值观念": "个体对新事物、新观念、怪异想法的态度;高分表现开放、坦然接受新事物,低分则相反", - }, - }, - "宜人性": { - "description": "反映个体在人际关系中的亲和倾向,体现了对他人的关心、同情和合作意愿。" - "这个维度主要关注个体与他人互动时的态度和行为特征,包括对他人的信任程度、同理心水平、" - "助人意愿以及在人际冲突中的处理方式。高分者通常表现出友善、富有同情心、乐于助人的特质,善于与他人建立和谐关系;" - "低分者则可能表现出较少的人际关注,在社交互动中更注重自身利益,较少考虑他人感受。", - "trait_words": ["友善", "同理", "信任", "合作"], - "subfactors": { - "信任": "个体对他人和/或他人言论的相信程度;高分表现信任他人,低分表现怀疑", - "体贴": "个体对别人的兴趣和需要的关注程度;高分表现体贴、温存,低分表现冷漠、不在乎", - "同情": "个体对处于不利地位的人或物的态度;高分表现富有同情心,低分表现冷漠", - }, - }, -} diff --git a/src/plugins/personality_s/renqingziji.py b/src/plugins/personality_s/renqingziji.py deleted file mode 100644 index ce4c268b8..000000000 --- a/src/plugins/personality_s/renqingziji.py +++ /dev/null @@ -1,195 +0,0 @@ -""" -The definition of artificial personality in this paper follows the dispositional para-digm and adapts a definition of -personality developed for humans [17]: -Personality for a human is the "whole and organisation of relatively stable tendencies and patterns of experience and -behaviour within one person (distinguishing it from other persons)". This definition is modified for artificial -personality: -Artificial personality describes the relatively stable tendencies and patterns of behav-iour of an AI-based machine that -can be designed by developers and designers via different modalities, such as language, creating the impression -of individuality of a humanized social agent when users interact with the machine.""" - -from typing import Dict, List -import json -import os -from pathlib import Path -from dotenv import load_dotenv -import sys - -""" -第一种方案:基于情景评估的人格测定 -""" -current_dir = Path(__file__).resolve().parent -project_root = current_dir.parent.parent.parent -env_path = project_root / ".env" - -root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) -sys.path.append(root_path) - -from src.plugins.personality.scene import get_scene_by_factor, PERSONALITY_SCENES # noqa: E402 -from src.plugins.personality.questionnaire import FACTOR_DESCRIPTIONS # noqa: E402 -from src.plugins.personality.offline_llm import LLMModel # noqa: E402 - -# 加载环境变量 -if env_path.exists(): - print(f"从 {env_path} 加载环境变量") - load_dotenv(env_path) -else: - print(f"未找到环境变量文件: {env_path}") - print("将使用默认配置") - - -class PersonalityEvaluatorDirect: - def __init__(self): - self.personality_traits = {"开放性": 0, "严谨性": 0, "外向性": 0, "宜人性": 0, "神经质": 0} - self.scenarios = [] - - # 为每个人格特质获取对应的场景 - for trait in PERSONALITY_SCENES: - scenes = get_scene_by_factor(trait) - if not scenes: - continue - - # 从每个维度选择3个场景 - import random - - scene_keys = list(scenes.keys()) - selected_scenes = random.sample(scene_keys, min(3, len(scene_keys))) - - for scene_key in selected_scenes: - scene = scenes[scene_key] - - # 为每个场景添加评估维度 - # 主维度是当前特质,次维度随机选择一个其他特质 - other_traits = [t for t in PERSONALITY_SCENES if t != trait] - secondary_trait = random.choice(other_traits) - - self.scenarios.append( - {"场景": scene["scenario"], "评估维度": [trait, secondary_trait], "场景编号": scene_key} - ) - - self.llm = LLMModel() - - def evaluate_response(self, scenario: str, response: str, dimensions: List[str]) -> Dict[str, float]: - """ - 使用 DeepSeek AI 评估用户对特定场景的反应 - """ - # 构建维度描述 - dimension_descriptions = [] - for dim in dimensions: - desc = FACTOR_DESCRIPTIONS.get(dim, "") - if desc: - dimension_descriptions.append(f"- {dim}:{desc}") - - dimensions_text = "\n".join(dimension_descriptions) - - prompt = f"""请根据以下场景和用户描述,评估用户在大五人格模型中的相关维度得分(1-6分)。 - -场景描述: -{scenario} - -用户回应: -{response} - -需要评估的维度说明: -{dimensions_text} - -请按照以下格式输出评估结果(仅输出JSON格式): -{{ - "{dimensions[0]}": 分数, - "{dimensions[1]}": 分数 -}} - -评分标准: -1 = 非常不符合该维度特征 -2 = 比较不符合该维度特征 -3 = 有点不符合该维度特征 -4 = 有点符合该维度特征 -5 = 比较符合该维度特征 -6 = 非常符合该维度特征 - -请根据用户的回应,结合场景和维度说明进行评分。确保分数在1-6之间,并给出合理的评估。""" - - try: - ai_response, _ = self.llm.generate_response(prompt) - # 尝试从AI响应中提取JSON部分 - start_idx = ai_response.find("{") - end_idx = ai_response.rfind("}") + 1 - if start_idx != -1 and end_idx != 0: - json_str = ai_response[start_idx:end_idx] - scores = json.loads(json_str) - # 确保所有分数在1-6之间 - return {k: max(1, min(6, float(v))) for k, v in scores.items()} - else: - print("AI响应格式不正确,使用默认评分") - return {dim: 3.5 for dim in dimensions} - except Exception as e: - print(f"评估过程出错:{str(e)}") - return {dim: 3.5 for dim in dimensions} - - -def main(): - print("欢迎使用人格形象创建程序!") - print("接下来,您将面对一系列场景(共15个)。请根据您想要创建的角色形象,描述在该场景下可能的反应。") - print("每个场景都会评估不同的人格维度,最终得出完整的人格特征评估。") - print("评分标准:1=非常不符合,2=比较不符合,3=有点不符合,4=有点符合,5=比较符合,6=非常符合") - print("\n准备好了吗?按回车键开始...") - input() - - evaluator = PersonalityEvaluatorDirect() - final_scores = {"开放性": 0, "严谨性": 0, "外向性": 0, "宜人性": 0, "神经质": 0} - dimension_counts = {trait: 0 for trait in final_scores.keys()} - - for i, scenario_data in enumerate(evaluator.scenarios, 1): - print(f"\n场景 {i}/{len(evaluator.scenarios)} - {scenario_data['场景编号']}:") - print("-" * 50) - print(scenario_data["场景"]) - print("\n请描述您的角色在这种情况下会如何反应:") - response = input().strip() - - if not response: - print("反应描述不能为空!") - continue - - print("\n正在评估您的描述...") - scores = evaluator.evaluate_response(scenario_data["场景"], response, scenario_data["评估维度"]) - - # 更新最终分数 - for dimension, score in scores.items(): - final_scores[dimension] += score - dimension_counts[dimension] += 1 - - print("\n当前评估结果:") - print("-" * 30) - for dimension, score in scores.items(): - print(f"{dimension}: {score}/6") - - if i < len(evaluator.scenarios): - print("\n按回车键继续下一个场景...") - input() - - # 计算平均分 - for dimension in final_scores: - if dimension_counts[dimension] > 0: - final_scores[dimension] = round(final_scores[dimension] / dimension_counts[dimension], 2) - - print("\n最终人格特征评估结果:") - print("-" * 30) - for trait, score in final_scores.items(): - print(f"{trait}: {score}/6") - print(f"测试场景数:{dimension_counts[trait]}") - - # 保存结果 - result = {"final_scores": final_scores, "dimension_counts": dimension_counts, "scenarios": evaluator.scenarios} - - # 确保目录存在 - os.makedirs("results", exist_ok=True) - - # 保存到文件 - with open("results/personality_result.json", "w", encoding="utf-8") as f: - json.dump(result, f, ensure_ascii=False, indent=2) - - print("\n结果已保存到 results/personality_result.json") - - -if __name__ == "__main__": - main() diff --git a/src/plugins/personality_s/who_r_u.py b/src/plugins/personality_s/who_r_u.py deleted file mode 100644 index 4877fb8c9..000000000 --- a/src/plugins/personality_s/who_r_u.py +++ /dev/null @@ -1,156 +0,0 @@ -import random -import os -import sys -from pathlib import Path -import datetime -from typing import List, Dict, Optional - -current_dir = Path(__file__).resolve().parent -project_root = current_dir.parent.parent.parent -env_path = project_root / ".env" - -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 - - -class MessageAnalyzer: - def __init__(self): - self.messages_collection = db["messages"] - - def get_message_context(self, message_id: int, context_length: int = 5) -> Optional[List[Dict]]: - """ - 获取指定消息ID的上下文消息列表 - - Args: - message_id (int): 消息ID - context_length (int): 上下文长度(单侧,总长度为 2*context_length + 1) - - Returns: - Optional[List[Dict]]: 消息列表,如果未找到则返回None - """ - # 从数据库获取指定消息 - target_message = self.messages_collection.find_one({"message_id": message_id}) - if not target_message: - return None - - # 获取该消息的stream_id - stream_id = target_message.get("chat_info", {}).get("stream_id") - if not stream_id: - return None - - # 获取同一stream_id的所有消息 - stream_messages = list(self.messages_collection.find({"chat_info.stream_id": stream_id}).sort("time", 1)) - - # 找到目标消息在列表中的位置 - target_index = None - for i, msg in enumerate(stream_messages): - if msg["message_id"] == message_id: - target_index = i - break - - if target_index is None: - return None - - # 获取目标消息前后的消息 - start_index = max(0, target_index - context_length) - end_index = min(len(stream_messages), target_index + context_length + 1) - - return stream_messages[start_index:end_index] - - def format_messages(self, messages: List[Dict], target_message_id: Optional[int] = None) -> str: - """ - 格式化消息列表为可读字符串 - - Args: - messages (List[Dict]): 消息列表 - target_message_id (Optional[int]): 目标消息ID,用于标记 - - Returns: - str: 格式化的消息字符串 - """ - if not messages: - return "没有消息记录" - - reply = "" - for msg in messages: - # 消息时间 - msg_time = datetime.datetime.fromtimestamp(int(msg["time"])).strftime("%Y-%m-%d %H:%M:%S") - - # 获取消息内容 - message_text = msg.get("processed_plain_text", msg.get("detailed_plain_text", "无消息内容")) - nickname = msg.get("user_info", {}).get("user_nickname", "未知用户") - - # 标记当前消息 - is_target = "→ " if target_message_id and msg["message_id"] == target_message_id else " " - - reply += f"{is_target}[{msg_time}] {nickname}: {message_text}\n" - - if target_message_id and msg["message_id"] == target_message_id: - reply += " " + "-" * 50 + "\n" - - return reply - - def get_user_random_contexts( - self, qq_id: str, num_messages: int = 10, context_length: int = 5 - ) -> tuple[List[str], str]: # noqa: E501 - """ - 获取用户的随机消息及其上下文 - - Args: - qq_id (str): QQ号 - num_messages (int): 要获取的随机消息数量 - context_length (int): 每条消息的上下文长度(单侧) - - Returns: - tuple[List[str], str]: (每个消息上下文的格式化字符串列表, 用户昵称) - """ - if not qq_id: - return [], "" - - # 获取用户所有消息 - all_messages = list(self.messages_collection.find({"user_info.user_id": int(qq_id)})) - if not all_messages: - return [], "" - - # 获取用户昵称 - user_nickname = all_messages[0].get("chat_info", {}).get("user_info", {}).get("user_nickname", "未知用户") - - # 随机选择指定数量的消息 - selected_messages = random.sample(all_messages, min(num_messages, len(all_messages))) - # 按时间排序 - selected_messages.sort(key=lambda x: int(x["time"])) - - # 存储所有上下文消息 - context_list = [] - - # 获取每条消息的上下文 - for msg in selected_messages: - message_id = msg["message_id"] - - # 获取消息上下文 - context_messages = self.get_message_context(message_id, context_length) - if context_messages: - formatted_context = self.format_messages(context_messages, message_id) - context_list.append(formatted_context) - - return context_list, user_nickname - - -if __name__ == "__main__": - # 测试代码 - analyzer = MessageAnalyzer() - test_qq = "1026294844" # 替换为要测试的QQ号 - print(f"测试QQ号: {test_qq}") - print("-" * 50) - # 获取5条消息,每条消息前后各3条上下文 - contexts, nickname = analyzer.get_user_random_contexts(test_qq, num_messages=5, context_length=3) - - print(f"用户昵称: {nickname}\n") - # 打印每个上下文 - for i, context in enumerate(contexts, 1): - print(f"\n随机消息 {i}/{len(contexts)}:") - print("-" * 30) - print(context) - print("=" * 50) diff --git a/src/plugins/personality_s/看我.txt b/src/plugins/personality_s/看我.txt deleted file mode 100644 index d5d6f8903..000000000 --- a/src/plugins/personality_s/看我.txt +++ /dev/null @@ -1 +0,0 @@ -那是以后会用到的妙妙小工具.jpg \ No newline at end of file From 0a74aba2ef19fd5c78470047ca8dce47cd7f6e93 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 16 Apr 2025 20:45:52 +0800 Subject: [PATCH 21/28] =?UTF-8?q?fix=EF=BC=9A=E6=9B=B4=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config_helper/config_helper.py | 477 ------------------ config_helper/config_notice.md | 10 - ...2请先看我.txt => 从0.6.0升级0.6.3请先看我.txt | 0 3 files changed, 487 deletions(-) delete mode 100644 config_helper/config_helper.py delete mode 100644 config_helper/config_notice.md rename 从0.6.0升级0.6.2请先看我.txt => 从0.6.0升级0.6.3请先看我.txt (100%) diff --git a/config_helper/config_helper.py b/config_helper/config_helper.py deleted file mode 100644 index b27aaead4..000000000 --- a/config_helper/config_helper.py +++ /dev/null @@ -1,477 +0,0 @@ -import os -import tomli -from packaging.specifiers import SpecifierSet -from packaging.version import Version -import sys - -import asyncio -import os -import time -from typing import Tuple, Union, AsyncGenerator, Generator - -import aiohttp -import requests -import json - - -class EnvInfo: - def __init__(self, env_path): - self.env_path = env_path - self.env_content_txt = None - self.env_content = {} - self.error_message = None - - - def check_env(self): - # 检查根目录是否存在.env文件 - if not os.path.exists(self.env_path): - self.error_message = "根目录没有.env文件,请自己创建或者运行一次MaiBot\n你可以直接复制template/template.env文件到根目录并重命名为.env" - return "not_found" - - #加载整个.env文件 - with open(self.env_path, "r", encoding="utf-8") as f: - self.env_content_txt = f.read() - - #逐行读取所有配置项 - for line in self.env_content_txt.splitlines(): - if line.strip() == "": - continue - key, value = line.split("=", 1) - self.env_content[key.strip()] = value.strip() - - # 检查.env文件的SILICONFLOW_KEY和SILICONFLOW_BASE_URL是否为空 - if "SILICONFLOW_KEY" not in self.env_content or "SILICONFLOW_BASE_URL" not in self.env_content: - if "DEEP_SEEK_BASE_URL" not in self.env_content or "DEEP_SEEK_KEY" not in self.env_content: - self.error_message = "没有设置可用的API和密钥,请检查.env文件,起码配置一个API来让帮助程序工作" - return "not_set" - else: - self.error_message = "你只设置了deepseek官方API,可能无法运行MaiBot,请检查.env文件" - return "only_ds" - - return "success" - -class LLM_request_off: - def __init__(self, model_name="deepseek-ai/DeepSeek-V3", env_info: EnvInfo = None, **kwargs): - self.model_name = model_name - self.params = kwargs - if model_name == "deepseek-ai/DeepSeek-V3" or model_name == "Pro/deepseek-ai/DeepSeek-V3": - self.api_key = env_info.env_content["SILICONFLOW_KEY"] - self.base_url = env_info.env_content["SILICONFLOW_BASE_URL"] - elif model_name == "deepseek-chat": - self.api_key = env_info.env_content["DEEP_SEEK_KEY"] - self.base_url = env_info.env_content["DEEP_SEEK_BASE_URL"] - # logger.info(f"API URL: {self.base_url}") # 使用 logger 记录 base_url - - def generate_stream(self, prompt: str) -> Generator[str, None, None]: - """流式生成模型响应""" - headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"} - - # 构建请求体,启用流式输出 - data = { - "model": self.model_name, - "messages": [{"role": "user", "content": prompt}], - "temperature": 0.4, - "stream": True, - **self.params, - } - - # 发送请求到完整的 chat/completions 端点 - api_url = f"{self.base_url.rstrip('/')}/chat/completions" - print(f"Stream Request URL: {api_url}") - - max_retries = 3 - base_wait_time = 15 - - for retry in range(max_retries): - try: - response = requests.post(api_url, headers=headers, json=data, stream=True) - - if response.status_code == 429: - wait_time = base_wait_time * (2**retry) - print(f"遇到请求限制(429),等待{wait_time}秒后重试...") - time.sleep(wait_time) - continue - - response.raise_for_status() - - # 处理流式响应 - for line in response.iter_lines(): - if line: - line = line.decode('utf-8') - if line.startswith('data: ') and not line.startswith('data: [DONE]'): - json_str = line[6:] # 去掉 "data: " 前缀 - try: - chunk_data = json.loads(json_str) - if ( - "choices" in chunk_data - and len(chunk_data["choices"]) > 0 - and "delta" in chunk_data["choices"][0] - and "content" in chunk_data["choices"][0]["delta"] - ): - content = chunk_data["choices"][0]["delta"]["content"] - yield content - except json.JSONDecodeError: - print(f"无法解析JSON: {json_str}") - return - - except Exception as e: - if retry < max_retries - 1: - wait_time = base_wait_time * (2**retry) - print(f"[流式回复]请求失败,等待{wait_time}秒后重试... 错误: {str(e)}") - time.sleep(wait_time) - else: - print(f"流式请求失败: {str(e)}") - yield f"流式请求失败: {str(e)}" - return - - print("达到最大重试次数,流式请求仍然失败") - yield "达到最大重试次数,流式请求仍然失败" - -class ConfigInfo: - def __init__(self, config_path): - self.config_path = config_path - self.config_content = "" - self.config_content_txt = None - self.template_content = None - self.version = None - self.error_message = None - - def check_bot_config(self): - """ - 检查config/bot_config.toml文件中是否有缺失项目 - """ - - if not os.path.exists(self.config_path): - self.error_message = f"错误:找不到配置文件 {self.config_path}" - return "not found" - - # 读取配置文件,先去掉注释,再解析TOML - try: - # 首先读取原始文件内容 - with open(self.config_path, "r", encoding="utf-8", errors="replace") as f: - file_content = f.read() - - # 去掉注释并保留有效内容 - cleaned_lines = [] - for line in file_content.splitlines(): - # 去掉行内注释 - if "#" in line: - line = line.split("#")[0].rstrip() - - # 如果行不是空的且不是以#开头的注释行,则添加到cleaned_lines - if line.strip() and not line.strip().startswith("#"): - cleaned_lines.append(line) - - # 将处理后的内容用于解析TOML - self.config_content_txt = "\n".join(cleaned_lines) - - # 使用tomli解析处理后的内容 - self.config_content = tomli.loads(self.config_content_txt) - except tomli.TOMLDecodeError as e: - self.error_message = f"错误:配置文件格式错误:{e}" - # 配置内容已经在上面设置了,不需要再次读取 - return "format_error" - except UnicodeDecodeError as e: - self.error_message = f"错误:配置文件编码错误,请使用UTF-8编码:{e}" - return "format_error" - - # 读取模板配置文件 - template_path = "template/bot_config_template.toml" - if not os.path.exists(template_path): - self.error_message = f"错误:找不到模板配置文件,请检查你是否启动过或者该程序是否位于根目录 {template_path}" - return "critical_error" - - try: - with open(template_path, "rb") as f: - template_content = tomli.load(f) - except Exception as e: - self.error_message = f"错误:无法解析模板配置文件,文件损坏,建议重新下载MaiBot:{e}" - return "critical_error" - - # 获取版本信息 - inner_version = self.config_content.get("inner", {}).get("version") - if not inner_version: - self.error_message = "错误:配置文件中缺少版本信息" - return "critical_error" - - try: - self.version = Version(inner_version) - except: - self.error_message = f"错误:版本号格式错误:{inner_version}" - return "critical_error" - - - # 检查所有必需的顶级配置项 - required_sections = [ - "bot", "groups", "personality", "identity", "platforms", - "response", "message", "willing", "emoji", "memory", - "mood", "model" - ] - - missing_sections = [] - for section in required_sections: - if section not in self.config_content: - missing_sections.append(section) - - if missing_sections: - self.error_message = f"错误:配置文件缺少以下顶级配置项:{', '.join(missing_sections)}" - return "critical_error" - - # 检查各个配置项内的必需字段 - missing_fields = [] - - # 检查bot配置 - if "bot" in self.config_content: - bot_config = self.config_content["bot"] - if "qq" not in bot_config: - missing_fields.append("bot.qq") - if "nickname" not in bot_config: - missing_fields.append("bot.nickname") - - # 检查groups配置 - if "groups" in self.config_content: - groups_config = self.config_content["groups"] - if "talk_allowed" not in groups_config: - missing_fields.append("groups.talk_allowed") - - # 检查platforms配置 - if "platforms" in self.config_content: - platforms_config = self.config_content["platforms"] - if not platforms_config or not isinstance(platforms_config, dict) or len(platforms_config) == 0: - missing_fields.append("platforms.(至少一个平台)") - - # 检查模型配置 - if "model" in self.config_content: - model_config = self.config_content["model"] - required_models = [ - "llm_reasoning", "llm_normal", "llm_topic_judge", - "llm_summary_by_topic", "llm_emotion_judge", "embedding", "vlm" - ] - - for model in required_models: - if model not in model_config: - missing_fields.append(f"model.{model}") - elif model in model_config: - model_item = model_config[model] - if "name" not in model_item: - missing_fields.append(f"model.{model}.name") - if "provider" not in model_item: - missing_fields.append(f"model.{model}.provider") - - # 基于模板检查其它必需字段 - def check_section(template_section, user_section, prefix): - if not isinstance(template_section, dict) or not isinstance(user_section, dict): - return - - for key, value in template_section.items(): - # 跳过注释和数组类型的配置项 - if key.startswith("#") or isinstance(value, list): - continue - - if key not in user_section: - missing_fields.append(f"{prefix}.{key}") - elif isinstance(value, dict) and key in user_section: - # 递归检查嵌套配置项 - check_section(value, user_section[key], f"{prefix}.{key}") - - for section in required_sections: - if section in template_content and section in self.config_content: - check_section(template_content[section], self.config_content[section], section) - - # 输出结果 - if missing_fields: - print(f"发现 {len(missing_fields)} 个缺失的配置项:") - for field in missing_fields: - print(f" - {field}") - else: - print("检查完成,没有发现缺失的必要配置项。") - - def get_value(self, path): - """ - 获取配置文件中指定路径的值 - 参数: - path: 以点分隔的路径,例如 "bot.qq" 或 "model.llm_normal.name" - 返回: - 找到的值,如果路径不存在则返回None - """ - if not self.config_content: - return None - - parts = path.split('.') - current = self.config_content - - try: - for part in parts: - if isinstance(current, dict) and part in current: - current = current[part] - else: - return None - return current - except: - return None - - def has_field(self, path): - """ - 检查配置文件中是否存在指定路径 - 参数: - path: 以点分隔的路径,例如 "bot.qq" 或 "model.llm_normal.name" - 返回: - 布尔值,表示路径是否存在 - """ - return self.get_value(path) is not None - - def get_section(self, section_name): - """ - 获取配置文件中的整个部分 - 参数: - section_name: 部分名称,例如 "bot" 或 "model" - 返回: - 字典形式的部分内容,如果部分不存在则返回空字典 - """ - if not self.config_content: - return {} - - return self.config_content.get(section_name, {}) - - def get_all_models(self): - """ - 获取配置中所有的模型配置 - 返回: - 包含所有模型配置的字典 - """ - if not self.config_content or "model" not in self.config_content: - return {} - - return self.config_content.get("model", {}) - - def __str__(self): - return f"配置文件路径:{self.config_path}\n配置文件版本:{self.version}\n错误信息:{self.error_message}" - -class ConfigHelper: - def __init__(self, config_info: ConfigInfo, model_using = "", env_info: EnvInfo = None): - self.config_info = config_info - self.config_notice = None - self.helper_model = LLM_request_off(model_name=model_using,env_info=env_info) - - def deal_format_error(self, error_message, config_content_txt): - prompt = f""" - 这里有一份配置文件存在格式错误,请检查配置文件为什么会出现该错误以及建议如何修改,不要使用markdown格式 - 错误信息:{error_message} - 配置文件内容:{config_content_txt} - 请根据错误信息和配置文件内容,用通俗易懂,简短的语言给出修改建议: - """ - - try: - # 使用流式输出获取分析结果 - print("\n===== 麦麦分析结果 =====") - for chunk in self.helper_model.generate_stream(prompt): - print(chunk, end="", flush=True) - print("\n=====================") - - except Exception as e: - print(f"请求麦麦分析时出错: {str(e)}") - print("请手动检查配置文件格式错误:", error_message) - - def load_config_notice(self): - with open(os.path.join(os.path.dirname(__file__), "config_notice.md"), "r", encoding="utf-8") as f: - self.config_notice = f.read() - - def deal_question(self, question): - prompt = f""" - 这里有一份配置文件,请根据问题给出回答 - 配置文件内容:{self.config_info.config_content_txt} - 关于配置文件的说明:{self.config_notice} - 问题:{question} - """ - - try: - # 使用流式输出获取分析结果 - print("\n===== 麦麦分析结果 =====") - for chunk in self.helper_model.generate_stream(prompt): - print(chunk, end="", flush=True) - print("\n=====================") - - except Exception as e: - print(f"请求麦麦分析时出错: {str(e)}") - - - -if __name__ == "__main__": - model_using = "deepseek-ai/DeepSeek-V3" - # model_using = "Pro/deepseek-ai/DeepSeek-V3" - env_info = EnvInfo(".env") - result = env_info.check_env() - if result == "not_set": - print(env_info.error_message) - exit() - elif result == "only_ds": - model_using = "deepseek-chat" - print("你只设置了deepseek官方API,可能无法运行MaiBot,但是你仍旧可以运行这个帮助程序,请检查.env文件") - elif result == "not_found": - print(env_info.error_message) - exit() - - config_path = "./config/bot_config.toml" - config_info = ConfigInfo(config_path) - print("开始检查config/bot_config.toml文件...") - result = config_info.check_bot_config() - print(config_info) - - helper = ConfigHelper(config_info, model_using, env_info) - helper.load_config_notice() - - # 如果配置文件读取成功,展示如何获取字段 - if config_info.config_content: - print("\n配置文件读取成功,可以访问任意字段:") - # 获取机器人昵称 - nickname = config_info.get_value("bot.nickname") - print(f"机器人昵称: {nickname}") - - # 获取QQ号 - qq = config_info.get_value("bot.qq") - print(f"机器人QQ: {qq}") - - # 获取群聊配置 - groups = config_info.get_section("groups") - print(f"允许聊天的群: {groups.get('talk_allowed', [])}") - - # 获取模型信息 - models = config_info.get_all_models() - print("\n模型配置信息:") - for model_name, model_info in models.items(): - provider = model_info.get("provider", "未知") - model_path = model_info.get("name", "未知") - print(f" - {model_name}: {model_path} (提供商: {provider})") - - # 检查某字段是否存在 - if config_info.has_field("model.llm_normal.temp"): - temp = config_info.get_value("model.llm_normal.temp") - print(f"\n回复模型温度: {temp}") - else: - print("\n回复模型温度未设置") - - # 获取心流相关设置 - if config_info.has_field("heartflow"): - heartflow = config_info.get_section("heartflow") - print(f"\n心流更新间隔: {heartflow.get('heart_flow_update_interval')}秒") - print(f"子心流更新间隔: {heartflow.get('sub_heart_flow_update_interval')}秒") - - if result == "critical_error": - print("配置文件存在严重错误,建议重新下载MaiBot") - exit() - elif result == "format_error": - print("配置文件格式错误,正在进行检查...") - error_message = config_info.error_message - config_content_txt = config_info.config_content_txt - helper.deal_format_error(error_message, config_content_txt) - else: - print("配置文件格式检查完成,没有发现问题") - - while True: - question = input("请输入你遇到的问题,麦麦会帮助你分析(输入exit退出):") - if question == "exit": - break - else: - print("麦麦正在为你分析...") - helper.deal_question(question) - diff --git a/config_helper/config_notice.md b/config_helper/config_notice.md deleted file mode 100644 index b704c8e47..000000000 --- a/config_helper/config_notice.md +++ /dev/null @@ -1,10 +0,0 @@ -1.Q:为什么我的bot叫他名字不回消息? -A:请检查qq和nickname字段是否正确填写 -请将默认字段: -qq = 114514 -nickname = "麦麦" -改为你自己的qq号和bot名称(需要与qq昵称相同) - -2. Q:如何修改日程表的内容,或者关闭日程表? -A:日程表目前无法关闭 -如果日程表生成的内容太过科幻或者疯癫,可以尝试调整日程表的温度或者修改日程表描述 \ No newline at end of file diff --git a/从0.6.0升级0.6.2请先看我.txt b/从0.6.0升级0.6.3请先看我.txt similarity index 100% rename from 从0.6.0升级0.6.2请先看我.txt rename to 从0.6.0升级0.6.3请先看我.txt From b4f284abca920e33833a3d163cdb6c4fba98e3c5 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 16 Apr 2025 21:10:34 +0800 Subject: [PATCH 22/28] =?UTF-8?q?fix=EF=BC=9A=E7=A7=BB=E5=8A=A8config?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE=EF=BC=8C=E4=BF=AE=E5=A4=8D=E9=A2=9C=E6=96=87?= =?UTF-8?q?=E5=AD=97bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/{plugins => }/config/auto_update.py | 0 src/{plugins => }/config/config.py | 4 +- src/do_tool/tool_can_use/change_mood.py | 2 +- src/do_tool/tool_use.py | 5 +- src/gui/logger_gui.py | 378 ------------------ src/gui/reasoning_gui.py | 342 ---------------- src/heart_flow/heartflow.py | 2 +- src/heart_flow/observation.py | 2 +- src/heart_flow/sub_heartflow.py | 4 +- src/main.py | 2 +- src/plugins/PFC/action_planner.py | 2 +- src/plugins/PFC/chat_observer.py | 2 +- src/plugins/PFC/pfc.py | 2 +- src/plugins/PFC/pfc_KnowledgeFetcher.py | 2 +- src/plugins/PFC/reply_checker.py | 2 +- src/plugins/PFC/reply_generator.py | 2 +- src/plugins/PFC/waiter.py | 2 +- src/plugins/chat/bot.py | 2 +- src/plugins/chat/emoji_manager.py | 2 +- src/plugins/chat/message_buffer.py | 2 +- src/plugins/chat/messagesender.py | 2 +- src/plugins/chat/utils.py | 58 ++- src/plugins/chat/utils_image.py | 2 +- .../only_process/only_message_process.py | 2 +- .../reasoning_chat/reasoning_chat.py | 2 +- .../reasoning_chat/reasoning_generator.py | 2 +- .../reasoning_prompt_builder.py | 2 +- .../think_flow_chat/think_flow_chat.py | 2 +- .../think_flow_chat/think_flow_generator.py | 2 +- .../think_flow_prompt_builder.py | 2 +- src/plugins/config/config_env.py | 59 --- src/plugins/memory_system/debug_memory.py | 2 +- src/plugins/models/utils_model.py | 2 +- src/plugins/moods/moods.py | 2 +- src/plugins/person_info/person_info.py | 2 +- src/plugins/remote/remote.py | 2 +- .../respon_info_catcher/info_catcher.py | 2 +- src/plugins/schedule/schedule_generator.py | 2 +- .../topic_identify/topic_identifier.py | 2 +- src/plugins/willing/mode_llmcheck.py | 2 +- src/plugins/willing/willing_manager.py | 2 +- 41 files changed, 86 insertions(+), 830 deletions(-) rename src/{plugins => }/config/auto_update.py (100%) rename src/{plugins => }/config/config.py (99%) delete mode 100644 src/gui/logger_gui.py delete mode 100644 src/gui/reasoning_gui.py delete mode 100644 src/plugins/config/config_env.py diff --git a/src/plugins/config/auto_update.py b/src/config/auto_update.py similarity index 100% rename from src/plugins/config/auto_update.py rename to src/config/auto_update.py diff --git a/src/plugins/config/config.py b/src/config/config.py similarity index 99% rename from src/plugins/config/config.py rename to src/config/config.py index ebde77734..332be7442 100644 --- a/src/plugins/config/config.py +++ b/src/config/config.py @@ -44,7 +44,7 @@ else: def update_config(): # 获取根目录路径 - root_dir = Path(__file__).parent.parent.parent.parent + root_dir = Path(__file__).parent.parent.parent template_dir = root_dir / "template" config_dir = root_dir / "config" old_config_dir = config_dir / "old" @@ -305,7 +305,7 @@ class BotConfig: def get_config_dir() -> str: """获取配置文件目录""" current_dir = os.path.dirname(os.path.abspath(__file__)) - root_dir = os.path.abspath(os.path.join(current_dir, "..", "..", "..")) + root_dir = os.path.abspath(os.path.join(current_dir, "..", "..")) config_dir = os.path.join(root_dir, "config") if not os.path.exists(config_dir): os.makedirs(config_dir) diff --git a/src/do_tool/tool_can_use/change_mood.py b/src/do_tool/tool_can_use/change_mood.py index 53410068f..1c13b1e5f 100644 --- a/src/do_tool/tool_can_use/change_mood.py +++ b/src/do_tool/tool_can_use/change_mood.py @@ -1,5 +1,5 @@ from src.do_tool.tool_can_use.base_tool import BaseTool -from src.plugins.config.config import global_config +from src.config.config import global_config from src.common.logger import get_module_logger from src.plugins.moods.moods import MoodManager from src.plugins.chat_module.think_flow_chat.think_flow_generator import ResponseGenerator diff --git a/src/do_tool/tool_use.py b/src/do_tool/tool_use.py index b8c35d912..b323f0452 100644 --- a/src/do_tool/tool_use.py +++ b/src/do_tool/tool_use.py @@ -1,5 +1,5 @@ from src.plugins.models.utils_model import LLMRequest -from src.plugins.config.config import global_config +from src.config.config import global_config from src.plugins.chat.chat_stream import ChatStream from src.common.database import db import time @@ -7,6 +7,8 @@ import json from src.common.logger import get_module_logger, TOOL_USE_STYLE_CONFIG, LogConfig from src.do_tool.tool_can_use import get_all_tool_definitions, get_tool_instance from src.heart_flow.sub_heartflow import SubHeartflow +import traceback +from src.plugins.chat.utils import get_recent_group_detailed_plain_text tool_use_config = LogConfig( # 使用消息发送专用样式 @@ -195,6 +197,7 @@ class ToolUser: except Exception as e: logger.error(f"工具调用过程中出错: {str(e)}") + logger.error(f"工具调用过程中出错: {traceback.format_exc()}") return { "used_tools": False, "error": str(e), diff --git a/src/gui/logger_gui.py b/src/gui/logger_gui.py deleted file mode 100644 index ad6edafb8..000000000 --- a/src/gui/logger_gui.py +++ /dev/null @@ -1,378 +0,0 @@ -# import customtkinter as ctk -# import subprocess -# import threading -# import queue -# import re -# import os -# import signal -# from collections import deque -# import sys - -# # 设置应用的外观模式和默认颜色主题 -# ctk.set_appearance_mode("dark") -# ctk.set_default_color_theme("blue") - - -# class LogViewerApp(ctk.CTk): -# """日志查看器应用的主类,继承自customtkinter的CTk类""" - -# def __init__(self): -# """初始化日志查看器应用的界面和状态""" -# super().__init__() -# self.title("日志查看器") -# self.geometry("1200x800") - -# # 标记GUI是否运行中 -# self.is_running = True - -# # 程序关闭时的清理操作 -# self.protocol("WM_DELETE_WINDOW", self._on_closing) - -# # 初始化进程、日志队列、日志数据等变量 -# self.process = None -# self.log_queue = queue.Queue() -# self.log_data = deque(maxlen=10000) # 使用固定长度队列 -# self.available_levels = set() -# self.available_modules = set() -# self.sorted_modules = [] -# self.module_checkboxes = {} # 存储模块复选框的字典 - -# # 日志颜色配置 -# self.color_config = { -# "time": "#888888", -# "DEBUG": "#2196F3", -# "INFO": "#4CAF50", -# "WARNING": "#FF9800", -# "ERROR": "#F44336", -# "module": "#D4D0AB", -# "default": "#FFFFFF", -# } - -# # 列可见性配置 -# self.column_visibility = {"show_time": True, "show_level": True, "show_module": True} - -# # 选中的日志等级和模块 -# self.selected_levels = set() -# self.selected_modules = set() - -# # 创建界面组件并启动日志队列处理 -# self.create_widgets() -# self.after(100, self.process_log_queue) - -# def create_widgets(self): -# """创建应用界面的各个组件""" -# self.grid_columnconfigure(0, weight=1) -# self.grid_rowconfigure(1, weight=1) - -# # 控制面板 -# control_frame = ctk.CTkFrame(self) -# control_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=5) - -# self.start_btn = ctk.CTkButton(control_frame, text="启动", command=self.start_process) -# self.start_btn.pack(side="left", padx=5) - -# self.stop_btn = ctk.CTkButton(control_frame, text="停止", command=self.stop_process, state="disabled") -# self.stop_btn.pack(side="left", padx=5) - -# self.clear_btn = ctk.CTkButton(control_frame, text="清屏", command=self.clear_logs) -# self.clear_btn.pack(side="left", padx=5) - -# column_filter_frame = ctk.CTkFrame(control_frame) -# column_filter_frame.pack(side="left", padx=20) - -# self.time_check = ctk.CTkCheckBox(column_filter_frame, text="显示时间", command=self.refresh_logs) -# self.time_check.pack(side="left", padx=5) -# self.time_check.select() - -# self.level_check = ctk.CTkCheckBox(column_filter_frame, text="显示等级", command=self.refresh_logs) -# self.level_check.pack(side="left", padx=5) -# self.level_check.select() - -# self.module_check = ctk.CTkCheckBox(column_filter_frame, text="显示模块", command=self.refresh_logs) -# self.module_check.pack(side="left", padx=5) -# self.module_check.select() - -# # 筛选面板 -# filter_frame = ctk.CTkFrame(self) -# filter_frame.grid(row=0, column=1, rowspan=2, sticky="ns", padx=5) - -# ctk.CTkLabel(filter_frame, text="日志等级筛选").pack(pady=5) -# self.level_scroll = ctk.CTkScrollableFrame(filter_frame, width=150, height=200) -# self.level_scroll.pack(fill="both", expand=True, padx=5) - -# ctk.CTkLabel(filter_frame, text="模块筛选").pack(pady=5) -# self.module_filter_entry = ctk.CTkEntry(filter_frame, placeholder_text="输入模块过滤词") -# self.module_filter_entry.pack(pady=5) -# self.module_filter_entry.bind("", self.update_module_filter) - -# self.module_scroll = ctk.CTkScrollableFrame(filter_frame, width=300, height=200) -# self.module_scroll.pack(fill="both", expand=True, padx=5) - -# self.log_text = ctk.CTkTextbox(self, wrap="word") -# self.log_text.grid(row=1, column=0, sticky="nsew", padx=10, pady=5) - -# self.init_text_tags() - -# def update_module_filter(self, event): -# """根据模块过滤词更新模块复选框的显示""" -# filter_text = self.module_filter_entry.get().strip().lower() -# for module, checkbox in self.module_checkboxes.items(): -# if filter_text in module.lower(): -# checkbox.pack(anchor="w", padx=5, pady=2) -# else: -# checkbox.pack_forget() - -# def update_filters(self, level, module): -# """更新日志等级和模块的筛选器""" -# if level not in self.available_levels: -# self.available_levels.add(level) -# self.add_checkbox(self.level_scroll, level, "level") - -# module_key = self.get_module_key(module) -# if module_key not in self.available_modules: -# self.available_modules.add(module_key) -# self.sorted_modules = sorted(self.available_modules, key=lambda x: x.lower()) -# self.rebuild_module_checkboxes() - -# def rebuild_module_checkboxes(self): -# """重新构建模块复选框""" -# # 清空现有复选框 -# for widget in self.module_scroll.winfo_children(): -# widget.destroy() -# self.module_checkboxes.clear() - -# # 重建排序后的复选框 -# for module in self.sorted_modules: -# self.add_checkbox(self.module_scroll, module, "module") - -# def add_checkbox(self, parent, text, type_): -# """在指定父组件中添加复选框""" - -# def update_filter(): -# current = cb.get() -# if type_ == "level": -# (self.selected_levels.add if current else self.selected_levels.discard)(text) -# else: -# (self.selected_modules.add if current else self.selected_modules.discard)(text) -# self.refresh_logs() - -# cb = ctk.CTkCheckBox(parent, text=text, command=update_filter) -# cb.select() # 初始选中 - -# # 手动同步初始状态到集合(关键修复) -# if type_ == "level": -# self.selected_levels.add(text) -# else: -# self.selected_modules.add(text) - -# if type_ == "module": -# self.module_checkboxes[text] = cb -# cb.pack(anchor="w", padx=5, pady=2) -# return cb - -# def check_filter(self, entry): -# """检查日志条目是否符合当前筛选条件""" -# level_ok = not self.selected_levels or entry["level"] in self.selected_levels -# module_key = self.get_module_key(entry["module"]) -# module_ok = not self.selected_modules or module_key in self.selected_modules -# return level_ok and module_ok - -# def init_text_tags(self): -# """初始化日志文本的颜色标签""" -# for tag, color in self.color_config.items(): -# self.log_text.tag_config(tag, foreground=color) -# self.log_text.tag_config("default", foreground=self.color_config["default"]) - -# def start_process(self): -# """启动日志进程并开始读取输出""" -# self.process = subprocess.Popen( -# ["nb", "run"], -# stdout=subprocess.PIPE, -# stderr=subprocess.STDOUT, -# text=True, -# bufsize=1, -# encoding="utf-8", -# errors="ignore", -# ) -# self.start_btn.configure(state="disabled") -# self.stop_btn.configure(state="normal") -# threading.Thread(target=self.read_output, daemon=True).start() - -# def stop_process(self): -# """停止日志进程并清理相关资源""" -# if self.process: -# try: -# if hasattr(self.process, "pid"): -# if os.name == "nt": -# subprocess.run( -# ["taskkill", "/F", "/T", "/PID", str(self.process.pid)], check=True, capture_output=True -# ) -# else: -# os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) -# except (subprocess.CalledProcessError, ProcessLookupError, OSError) as e: -# print(f"终止进程失败: {e}") -# finally: -# self.process = None -# self.log_queue.queue.clear() -# self.start_btn.configure(state="normal") -# self.stop_btn.configure(state="disabled") -# self.refresh_logs() - -# def read_output(self): -# """读取日志进程的输出并放入队列""" -# try: -# while self.process and self.process.poll() is None and self.is_running: -# line = self.process.stdout.readline() -# if line: -# self.log_queue.put(line) -# else: -# break # 避免空循环 -# self.process.stdout.close() # 确保关闭文件描述符 -# except ValueError: # 处理可能的I/O操作异常 -# pass - -# def process_log_queue(self): -# """处理日志队列中的日志条目""" -# while not self.log_queue.empty(): -# line = self.log_queue.get() -# self.process_log_line(line) - -# # 仅在GUI仍在运行时继续处理队列 -# if self.is_running: -# self.after(100, self.process_log_queue) - -# def process_log_line(self, line): -# """解析单行日志并更新日志数据和筛选器""" -# match = re.match( -# r"""^ -# (?:(?P