Merge branch 'HFC-test' of https://github.com/smartmita/MaiBot into G-Test

pull/937/head
Bakadax 2025-05-13 18:55:56 +08:00
commit bd3df844be
40 changed files with 2055 additions and 1152 deletions

4
.gitignore vendored
View File

@ -13,6 +13,7 @@ llm_tool_benchmark_results.json
MaiBot-Napcat-Adapter-main
MaiBot-Napcat-Adapter
/test
/log_debug
/src/test
nonebot-maibot-adapter/
*.zip
@ -35,7 +36,6 @@ config/bot_config.toml
config/bot_config.toml.bak
config/lpmm_config.toml
config/lpmm_config.toml.bak
src/plugins/remote/client_uuid.json
(测试版)麦麦生成人格.bat
(临时版)麦麦开始学习.bat
src/plugins/utils/statistic.py
@ -43,7 +43,7 @@ src/plugins/utils/statistic.py
__pycache__/
*.py[cod]
*$py.class
llm_statistics.txt
maibot_statistics.html
mongodb
napcat
run_dev.bat

43
bot.py
View File

@ -1,7 +1,6 @@
import asyncio
import hashlib
import os
import shutil
import sys
from pathlib import Path
import time
@ -17,6 +16,8 @@ from rich.traceback import install
from src.plugins.group_nickname.nickname_manager import nickname_manager
import atexit
from src.manager.async_task_manager import async_task_manager
install(extra_lines=3)
# 设置工作目录为脚本所在目录
@ -66,38 +67,6 @@ def easter_egg():
print(rainbow_text)
def init_config():
# 初次启动检测
if not os.path.exists("config/bot_config.toml"):
logger.warning("检测到bot_config.toml不存在正在从模板复制")
# 检查config目录是否存在
if not os.path.exists("config"):
os.makedirs("config")
logger.info("创建config目录")
shutil.copy("template/bot_config_template.toml", "config/bot_config.toml")
logger.info("复制完成请修改config/bot_config.toml和.env中的配置后重新启动")
if not os.path.exists("config/lpmm_config.toml"):
logger.warning("检测到lpmm_config.toml不存在正在从模板复制")
# 检查config目录是否存在
if not os.path.exists("config"):
os.makedirs("config")
logger.info("创建config目录")
shutil.copy("template/lpmm_config_template.toml", "config/lpmm_config.toml")
logger.info("复制完成请修改config/lpmm_config.toml和.env中的配置后重新启动")
def init_env():
# 检测.env文件是否存在
if not os.path.exists(".env"):
logger.error("检测到.env文件不存在")
shutil.copy("template/template.env", "./.env")
logger.info("已从template/template.env复制创建.env请修改配置后重新启动")
def load_env():
# 直接加载生产环境变量配置
if os.path.exists(".env"):
@ -142,6 +111,10 @@ def scan_provider(env_config: dict):
async def graceful_shutdown():
try:
logger.info("正在优雅关闭麦麦...")
# 停止所有异步任务
await async_task_manager.stop_and_wait_all_tasks()
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
for task in tasks:
task.cancel()
@ -237,9 +210,9 @@ def raw_main():
check_eula()
print("检查EULA和隐私条款完成")
easter_egg()
init_config()
init_env()
load_env()
env_config = {key: os.getenv(key) for key in os.environ}

View File

@ -1,3 +1,6 @@
# TODO: 更多的可配置项
# TODO: 所有模型单独分离,温度可配置
# TODO: 原生多模态支持
import os
import re
from dataclasses import dataclass, field
@ -277,18 +280,44 @@ class BotConfig:
# experimental
enable_friend_chat: bool = False # 是否启用好友聊天
# enable_think_flow: bool = False # 是否启用思考流程
enable_friend_whitelist: bool = True # 是否启用好友白名单
talk_allowed_private = set()
enable_pfc_chatting: bool = False # 是否启用PFC聊天
enable_pfc_reply_checker: bool = True # 是否开启PFC回复检查
pfc_message_buffer_size: int = (
2 # PFC 聊天消息缓冲数量,有利于使聊天节奏更加紧凑流畅,请根据实际 LLM 响应速度进行调整默认2条
)
api_polling_max_retries: int = 3 # 神秘小功能
rename_person: bool = (
True # 是否启用改名工具,可以让麦麦对唯一名进行更改,可能可以更拟人地称呼他人,但是也可能导致记忆混淆的问题
)
# idle_chat
# pfc
enable_pfc_chatting: bool = False # 是否启用PFC聊天该功能仅作用于私聊与回复模式独立
pfc_message_buffer_size: int = (
2 # PFC 聊天消息缓冲数量,有利于使聊天节奏更加紧凑流畅,请根据实际 LLM 响应速度进行调整默认2条
)
pfc_recent_history_display_count: int = 20 # PFC 对话最大可见上下文
# pfc.checker
enable_pfc_reply_checker: bool = True # 是否启用 PFC 的回复检查器
pfc_max_reply_attempts: int = 3 # 发言最多尝试次数
pfc_max_chat_history_for_checker: int = 50 # checker聊天记录最大可见上文长度
# pfc.emotion
pfc_emotion_update_intensity: float = 0.6 # 情绪更新强度
pfc_emotion_history_count: int = 5 # 情绪更新最大可见上下文长度
# pfc.relationship
pfc_relationship_incremental_interval: int = 10 # 关系值增值强度
pfc_relationship_incremental_msg_count: int = 10 # 会话中,关系值判断最大可见上下文
pfc_relationship_incremental_default_change: float = (
1.0 # 会话中,关系值默认更新值(当 llm 返回错误时默认采用该值)
)
pfc_relationship_incremental_max_change: float = 5.0 # 会话中,关系值最大可变值
pfc_relationship_final_msg_count: int = 30 # 会话结束时,关系值判断最大可见上下文
pfc_relationship_final_default_change: float = 5.0 # 会话结束时,关系值默认更新值
pfc_relationship_final_max_change: float = 50.0 # 会话结束时,关系值最大可变值
# pfc.fallback
pfc_historical_fallback_exclude_seconds: int = 7200 # pfc 翻看聊天记录排除最近时长
# pfc.idle_chat
enable_idle_chat: bool = False # 是否启用 pfc 主动发言
idle_check_interval: int = 10 # 检查间隔10分钟检查一次
min_cooldown: int = 7200 # 最短冷却时间2小时 (7200秒)
@ -301,6 +330,7 @@ class BotConfig:
nickname_queue_max_size: int = 100 # 绰号处理队列最大容量
nickname_process_sleep_interval: float = 5 # 绰号处理进程休眠间隔(秒)
nickname_analysis_history_limit: int = 30 # 绰号处理可见最大上下文
nickname_analysis_probability: float = 0.1 # 绰号随机概率命中,该值越大,绰号分析越频繁
# 模型配置
llm_reasoning: dict[str, str] = field(default_factory=lambda: {})
@ -458,6 +488,9 @@ class BotConfig:
config.nickname_analysis_history_limit = group_nickname_config.get(
"nickname_analysis_history_limit", config.nickname_analysis_history_limit
)
config.nickname_analysis_probability = group_nickname_config.get(
"nickname_analysis_probability", config.nickname_analysis_probability
)
def bot(parent: dict):
# 机器人基础配置
@ -547,10 +580,10 @@ class BotConfig:
"llm_heartflow",
"llm_PFC_action_planner",
"llm_PFC_chat",
"llm_PFC_relationship_eval",
"llm_nickname_mapping",
"llm_scheduler_all",
"llm_scheduler_doing",
"llm_PFC_relationship_eval",
]
for item in config_list:
@ -724,30 +757,97 @@ class BotConfig:
config.enable_friend_chat = experimental_config.get("enable_friend_chat", config.enable_friend_chat)
# config.enable_think_flow = experimental_config.get("enable_think_flow", config.enable_think_flow)
config.talk_allowed_private = set(str(user) for user in experimental_config.get("talk_allowed_private", []))
if config.INNER_VERSION in SpecifierSet(">=1.1.0"):
config.enable_pfc_chatting = experimental_config.get("pfc_chatting", config.enable_pfc_chatting)
if config.INNER_VERSION in SpecifierSet(">=1.6.2.4"):
config.enable_friend_whitelist = experimental_config.get(
"enable_friend_whitelist", config.enable_friend_whitelist
)
if config.INNER_VERSION in SpecifierSet(">=1.6.1.5"):
config.api_polling_max_retries = experimental_config.get(
"api_polling_max_retries", config.api_polling_max_retries
)
if config.INNER_VERSION in SpecifierSet(">=1.6.2"):
config.enable_pfc_reply_checker = experimental_config.get(
"enable_pfc_reply_checker", config.enable_pfc_reply_checker
)
logger.info(f"PFC Reply Checker 状态: {'启用' if config.enable_pfc_reply_checker else '关闭'}")
config.pfc_message_buffer_size = experimental_config.get(
"pfc_message_buffer_size", config.pfc_message_buffer_size
)
if config.INNER_VERSION in SpecifierSet(">=1.6.2.3"):
config.rename_person = experimental_config.get("rename_person", config.rename_person)
def idle_chat(parent: dict):
idle_chat_config = parent["idle_chat"]
if config.INNER_VERSION in SpecifierSet(">=1.6.1.6"):
config.enable_idle_chat = idle_chat_config.get("enable_idle_chat", config.enable_idle_chat)
config.idle_check_interval = idle_chat_config.get("idle_check_interval", config.idle_check_interval)
config.min_cooldown = idle_chat_config.get("min_cooldown", config.min_cooldown)
config.max_cooldown = idle_chat_config.get("max_cooldown", config.max_cooldown)
def pfc(parent: dict):
if config.INNER_VERSION in SpecifierSet(">=1.6.2.4"):
pfc_config = parent.get("pfc", {})
# 解析 [pfc] 下的直接字段
config.enable_pfc_chatting = pfc_config.get("enable_pfc_chatting", config.enable_pfc_chatting)
config.pfc_message_buffer_size = pfc_config.get(
"pfc_message_buffer_size", config.pfc_message_buffer_size
)
config.pfc_recent_history_display_count = pfc_config.get(
"pfc_recent_history_display_count", config.pfc_recent_history_display_count
)
# 解析 [[pfc.checker]] 子表
checker_list = pfc_config.get("checker", [])
if checker_list and isinstance(checker_list, list):
checker_config = checker_list[0] if checker_list else {}
config.enable_pfc_reply_checker = checker_config.get(
"enable_pfc_reply_checker", config.enable_pfc_reply_checker
)
config.pfc_max_reply_attempts = checker_config.get(
"pfc_max_reply_attempts", config.pfc_max_reply_attempts
)
config.pfc_max_chat_history_for_checker = checker_config.get(
"pfc_max_chat_history_for_checker", config.pfc_max_chat_history_for_checker
)
# 解析 [[pfc.emotion]] 子表
emotion_list = pfc_config.get("emotion", [])
if emotion_list and isinstance(emotion_list, list):
emotion_config = emotion_list[0] if emotion_list else {}
config.pfc_emotion_update_intensity = emotion_config.get(
"pfc_emotion_update_intensity", config.pfc_emotion_update_intensity
)
config.pfc_emotion_history_count = emotion_config.get(
"pfc_emotion_history_count", config.pfc_emotion_history_count
)
# 解析 [[pfc.relationship]] 子表
relationship_list = pfc_config.get("relationship", [])
if relationship_list and isinstance(relationship_list, list):
relationship_config = relationship_list[0] if relationship_list else {}
config.pfc_relationship_incremental_interval = relationship_config.get(
"pfc_relationship_incremental_interval", config.pfc_relationship_incremental_interval
)
config.pfc_relationship_incremental_msg_count = relationship_config.get(
"pfc_relationship_incremental_msg_count", config.pfc_relationship_incremental_msg_count
)
config.pfc_relationship_incremental_default_change = relationship_config.get(
"pfc_relationship_incremental_default_change",
config.pfc_relationship_incremental_default_change,
)
config.pfc_relationship_incremental_max_change = relationship_config.get(
"pfc_relationship_incremental_max_change", config.pfc_relationship_incremental_max_change
)
config.pfc_relationship_final_msg_count = relationship_config.get(
"pfc_relationship_final_msg_count", config.pfc_relationship_final_msg_count
)
config.pfc_relationship_final_default_change = relationship_config.get(
"pfc_relationship_final_default_change", config.pfc_relationship_final_default_change
)
config.pfc_relationship_final_max_change = relationship_config.get(
"pfc_relationship_final_max_change", config.pfc_relationship_final_max_change
)
# 解析 [[pfc.fallback]] 子表
fallback_list = pfc_config.get("fallback", [])
if fallback_list and isinstance(fallback_list, list):
fallback_config = fallback_list[0] if fallback_list else {}
config.pfc_historical_fallback_exclude_seconds = fallback_config.get(
"pfc_historical_fallback_exclude_seconds", config.pfc_historical_fallback_exclude_seconds
)
# 解析 [[pfc.idle_chat]] 子表
idle_chat_list = pfc_config.get("idle_chat", [])
if idle_chat_list and isinstance(idle_chat_list, list):
idle_chat_config = idle_chat_list[0] if idle_chat_list else {}
config.enable_idle_chat = idle_chat_config.get("enable_idle_chat", config.enable_idle_chat)
config.idle_check_interval = idle_chat_config.get("idle_check_interval", config.idle_check_interval)
config.min_cooldown = idle_chat_config.get("min_cooldown", config.min_cooldown)
config.max_cooldown = idle_chat_config.get("max_cooldown", config.max_cooldown)
# 版本表达式:>=1.0.0,<2.0.0
# 允许字段func: method, support: str, notice: str, necessary: bool
@ -783,7 +883,7 @@ class BotConfig:
"normal_chat": {"func": normal_chat, "support": ">=1.6.0", "necessary": False},
"focus_chat": {"func": focus_chat, "support": ">=1.6.0", "necessary": False},
"group_nickname": {"func": group_nickname, "support": ">=1.6.1.1", "necessary": False},
"idle_chat": {"func": idle_chat, "support": ">=1.6.1.6", "necessary": False},
"pfc": {"func": pfc, "support": ">=1.6.2.4", "necessary": False},
}
# 原地修改,将 字符串版本表达式 转换成 版本对象

View File

@ -1,10 +1,10 @@
from src.do_tool.tool_can_use.base_tool import BaseTool
from src.config.config import global_config
from src.common.logger_manager import get_logger
from src.plugins.moods.moods import MoodManager
from typing import Any
from src.common.logger_manager import get_logger
from src.config.config import global_config
from src.do_tool.tool_can_use.base_tool import BaseTool
from src.manager.mood_manager import mood_manager
logger = get_logger("change_mood_tool")
@ -36,7 +36,6 @@ class ChangeMoodTool(BaseTool):
response_set = function_args.get("response_set")
_message_processed_plain_text = function_args.get("text")
mood_manager = MoodManager.get_instance()
# gpt = ResponseGenerator()
if response_set is None:

View File

@ -1,4 +1,4 @@
from src.plugins.moods.moods import MoodManager
from src.manager.mood_manager import mood_manager
import enum
@ -13,5 +13,5 @@ class ChatStateInfo:
self.chat_status: ChatState = ChatState.ABSENT
self.current_state_time = 120
self.mood_manager = MoodManager()
self.mood = self.mood_manager.get_prompt()
self.mood_manager = mood_manager
self.mood = self.mood_manager.get_mood_prompt()

View File

@ -3,7 +3,7 @@ import time
import random
from typing import List, Tuple, Optional
from src.common.logger_manager import get_logger
from src.plugins.moods.moods import MoodManager
from src.manager.mood_manager import mood_manager
from src.config.config import global_config
logger = get_logger("mai_state")
@ -88,7 +88,7 @@ class MaiStateInfo:
self.last_min_check_time: float = time.time() # 上次1分钟规则检查时间
# Mood management is now part of MaiStateInfo
self.mood_manager = MoodManager.get_instance() # Use singleton instance
self.mood_manager = mood_manager # Use singleton instance
def update_mai_status(self, new_status: MaiState) -> bool:
"""
@ -124,7 +124,7 @@ class MaiStateInfo:
def get_mood_prompt(self) -> str:
"""获取当前的心情提示词"""
# Delegate to the internal mood manager
return self.mood_manager.get_prompt()
return self.mood_manager.get_mood_prompt()
def get_current_state(self) -> MaiState:
"""获取当前的 MaiState"""

View File

@ -95,7 +95,7 @@ class ChattingObservation(Observation):
if ids:
mid_memory_str = ""
for id in ids:
print(f"id{id}")
# print(f"id{id}")
try:
for mid_memory in self.mid_memorys:
if mid_memory["id"] == id:

View File

@ -1,7 +1,10 @@
from .observation import ChattingObservation
from src.plugins.knowledge.knowledge_lib import qa_manager
from src.plugins.models.utils_model import LLMRequest
from src.config.config import global_config
from src.plugins.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat, build_readable_messages
import time
import re
import traceback
from src.common.logger_manager import get_logger
from src.individuality.individuality import Individuality
@ -24,18 +27,35 @@ logger = get_logger("sub_heartflow")
def init_prompt():
# --- Group Chat Prompt ---
group_prompt = """
{extra_info}
{relation_prompt}
你的名字是{bot_name},{prompt_personality}
{last_loop_prompt}
{cycle_info_block}
现在是{time_now}你正在上网和qq群里的网友们聊天以下是正在进行的聊天内容
{chat_observe_info}
<identity>
<bot_name>你的名字是{bot_name}</bot_name>
<personality_profile>{prompt_personality}</personality_profile>
</identity>
你现在{mood_info}
请仔细阅读当前群聊内容分析讨论话题和群成员关系分析你刚刚发言和别人对你的发言的反应思考你要不要回复或发言然后思考你是否需要使用函数工具
思考并输出你的内心想法
输出要求
<knowledge_base>
<structured_information>{extra_info}</structured_information>
<social_relationships>{relation_prompt}</social_relationships>
</knowledge_base>
<recent_internal_state>
<previous_thoughts_and_actions>{last_loop_prompt}</previous_thoughts_and_actions>
<recent_reply_history>{cycle_info_block}</recent_reply_history>
<current_mood>你现在{mood_info}</current_mood>
</recent_internal_state>
<live_chat_context>
<timestamp>现在是{time_now}</timestamp>
<chat_log>你正在上网和qq群里的网友们聊天以下是正在进行的聊天内容
{chat_observe_info}</chat_log>
</live_chat_context>
<thinking_guidance>
请仔细阅读当前聊天内容分析讨论话题和群成员关系分析你刚刚发言和别人对你的发言的反应思考你要不要回复或发言然后思考你是否需要使用函数工具
思考并输出你真实的内心想法
</thinking_guidance>
<output_requirements_for_inner_thought>
1. 根据聊天内容生成你的想法{hf_do_next}
2. 不要分点不要使用表情符号
3. 避免多余符号(冒号引号括号等)
@ -43,11 +63,17 @@ def init_prompt():
5. 如果你刚发言并且没有人回复你请谨慎考虑要不要继续发消息
6. 不要把注意力放在别人发的表情包上它们只是一种辅助表达方式
7. 注意分辨群里谁在跟谁说话你不一定是当前聊天的主角消息中的不一定指的是你{bot_name}也可能是别人
8. 思考要不要回复或发言如果要需要思考一下具体说什么
工具使用说明
8. 思考要不要回复或发言如果要必须**明确写出**你准备发送的消息的具体内容是什么
9. 默认使用中文
</output_requirements_for_inner_thought>
<tool_usage_instructions>
1. 输出想法后考虑是否需要使用工具
2. 工具可获取信息或执行操作
3. 如需处理消息或回复请使用工具"""
3. 如需处理消息或回复请使用工具
</tool_usage_instructions>
"""
Prompt(group_prompt, "sub_heartflow_prompt_before")
# --- Private Chat Prompt ---
@ -85,6 +111,35 @@ def init_prompt():
Prompt(last_loop_t, "last_loop")
def parse_knowledge_and_get_max_relevance(knowledge_str: str) -> str | float:
"""
解析 qa_manager.get_knowledge 返回的字符串提取所有知识的文本和最高的相关性得分
返回: (原始知识字符串, 最高相关性得分)如果无有效相关性则返回 (原始知识字符串, 0.0)
"""
if not knowledge_str:
return None, 0.0
max_relevance = 0.0
# 正则表达式匹配 "该条知识对于问题的相关性:数字"
# 我们需要捕获数字部分
relevance_scores = re.findall(r"该条知识对于问题的相关性:([0-9.]+)", knowledge_str)
if relevance_scores:
try:
max_relevance = max(float(score) for score in relevance_scores)
except ValueError:
logger.warning(f"解析相关性得分时出错: {relevance_scores}")
return knowledge_str, 0.0 # 出错时返回0.0
else:
# 如果没有找到 "该条知识对于问题的相关性:" 这样的模式,
# 说明可能 qa_manager 返回的格式有变,或者没有有效的知识。
# 在这种情况下我们无法确定相关性保守起见返回0.0
logger.debug(f"在知识字符串中未找到明确的相关性得分标记: '{knowledge_str[:100]}...'")
return knowledge_str, 0.0
return knowledge_str, max_relevance
def calculate_similarity(text_a: str, text_b: str) -> float:
"""
计算两个文本字符串的相似度
@ -127,7 +182,7 @@ class SubMind:
self.llm_model = LLMRequest(
model=global_config.llm_sub_heartflow,
temperature=global_config.llm_sub_heartflow["temp"],
max_tokens=800,
max_tokens=1000,
request_type="sub_heart_flow",
)
@ -142,6 +197,14 @@ class SubMind:
name = chat_manager.get_stream_name(self.subheartflow_id)
self.log_prefix = f"[{name}] "
self._update_structured_info_str()
# 阶梯式筛选
self.knowledge_retrieval_steps = self.knowledge_retrieval_steps = [
{"name": "latest_1_msg", "limit": 1, "relevance_threshold": 0.75}, # 新增最新1条极高阈值
{"name": "latest_2_msgs", "limit": 2, "relevance_threshold": 0.65}, # 新增最新2条较高阈值
{"name": "short_window_3_msgs", "limit": 3, "relevance_threshold": 0.50}, # 原有的3条阈值可保持或微调
{"name": "medium_window_8_msgs", "limit": 8, "relevance_threshold": 0.30}, # 原有的8条阈值可保持或微调
# 完整窗口的回退逻辑保持不变
]
def _update_structured_info_str(self):
"""根据 structured_info 更新 structured_info_str"""
@ -184,23 +247,26 @@ class SubMind:
# ---------- 0. 更新和清理 structured_info ----------
if self.structured_info:
logger.debug(
f"{self.log_prefix} 更新前的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}"
f"{self.log_prefix} 清理前 structured_info 中包含的lpmm_knowledge数量: "
f"{len([item for item in self.structured_info if item.get('type') == 'lpmm_knowledge'])}"
)
updated_info = []
for item in self.structured_info:
# 筛选出所有不是 lpmm_knowledge 类型的条目,或者其他需要保留的条目
info_to_keep = [item for item in self.structured_info if item.get("type") != "lpmm_knowledge"]
# 针对我们仅希望 lpmm_knowledge "用完即弃" 的情况:
processed_info_to_keep = []
for item in info_to_keep: # info_to_keep 已经不包含 lpmm_knowledge
item["ttl"] -= 1
if item["ttl"] > 0:
updated_info.append(item)
processed_info_to_keep.append(item)
else:
logger.debug(f"{self.log_prefix} 移除过期的 structured_info 项: {item['id']}")
self.structured_info = updated_info
logger.debug(f"{self.log_prefix} 移除过期的非lpmm_knowledge项: {item.get('id', '未知ID')}")
self.structured_info = processed_info_to_keep
logger.debug(
f"{self.log_prefix} 更新后的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}"
f"{self.log_prefix} 清理后 structured_info (仅保留非lpmm_knowledge且TTL有效项): "
f"{safe_json_dumps(self.structured_info, ensure_ascii=False)}"
)
self._update_structured_info_str()
logger.debug(
f"{self.log_prefix} 当前完整的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}"
)
# ---------- 1. 准备基础数据 ----------
# 获取现有想法和情绪状态
@ -270,6 +336,117 @@ class SubMind:
logger.error(f"{self.log_prefix} 获取记忆时出错: {e}")
logger.error(traceback.format_exc())
# ---------- 2.5 阶梯式获取知识库信息 ----------
final_knowledge_to_add = None
retrieval_source_info = "未进行知识检索"
# 确保 observation 对象存在且可用
if not observation:
logger.warning(f"{self.log_prefix} Observation 对象不可用,跳过知识库检索。")
else:
# 阶段1和阶段2的阶梯检索
for step_config in self.knowledge_retrieval_steps:
step_name = step_config["name"]
limit = step_config["limit"]
threshold = step_config["relevance_threshold"]
logger.info(f"{self.log_prefix} 尝试阶梯检索 - 阶段: {step_name} (最近{limit}条, 阈值>{threshold})")
try:
# 1. 获取当前阶段的聊天记录上下文
# 我们需要从 observation 中获取原始消息列表来构建特定长度的上下文
# get_raw_msg_before_timestamp_with_chat 在 observation.py 中被导入
# from src.plugins.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat, build_readable_messages
# 需要确保 ChattingObservation 的实例 (self.observations[0]) 能提供 chat_id
# 并且 build_readable_messages 可用
context_messages_dicts = get_raw_msg_before_timestamp_with_chat(
chat_id=observation.chat_id, timestamp=time.time(), limit=limit
)
if not context_messages_dicts:
logger.debug(f"{self.log_prefix} 阶段 '{step_name}' 未获取到聊天记录,跳过此阶段。")
continue
current_context_text = await build_readable_messages(
messages=context_messages_dicts,
timestamp_mode="lite", # 或者您认为适合知识检索的模式
)
if not current_context_text:
logger.debug(f"{self.log_prefix} 阶段 '{step_name}' 构建的上下文为空,跳过此阶段。")
continue
logger.debug(f"{self.log_prefix} 阶段 '{step_name}' 使用上下文: '{current_context_text[:150]}...'")
# 2. 调用知识库进行检索
raw_knowledge_str = qa_manager.get_knowledge(current_context_text)
if raw_knowledge_str:
# 3. 解析知识并检查相关性
knowledge_content, max_relevance = parse_knowledge_and_get_max_relevance(raw_knowledge_str)
logger.info(f"{self.log_prefix} 阶段 '{step_name}' 检索到知识,最高相关性: {max_relevance:.4f}")
if max_relevance >= threshold:
logger.info(
f"{self.log_prefix} 阶段 '{step_name}' 满足阈值 ({max_relevance:.4f} >= {threshold}),采纳此知识。"
)
final_knowledge_to_add = knowledge_content
retrieval_source_info = f"阶段 '{step_name}' (最近{limit}条, 相关性 {max_relevance:.4f})"
break # 找到符合条件的知识,跳出阶梯循环
else:
logger.info(
f"{self.log_prefix} 阶段 '{step_name}' 未满足阈值 ({max_relevance:.4f} < {threshold}),继续下一阶段。"
)
else:
logger.debug(f"{self.log_prefix} 阶段 '{step_name}' 未从知识库检索到任何内容。")
except Exception as e_step:
logger.error(f"{self.log_prefix} 阶梯检索阶段 '{step_name}' 发生错误: {e_step}")
logger.error(traceback.format_exc())
continue # 当前阶段出错,尝试下一阶段
# 阶段3: 如果前面的阶梯都没有成功,则使用完整的 chat_observe_info (即您配置的20条)
if not final_knowledge_to_add and chat_observe_info: # 确保 chat_observe_info 可用
logger.info(
f"{self.log_prefix} 前序阶梯均未满足条件,尝试使用完整观察窗口 ('{observation.max_now_obs_len}'条)进行检索。"
)
try:
raw_knowledge_str = qa_manager.get_knowledge(chat_observe_info)
if raw_knowledge_str:
# 对于完整窗口,我们可能不强制要求阈值,或者使用一个较低的阈值
# 或者,您可以选择在这里仍然应用一个阈值,例如 self.knowledge_retrieval_steps 中最后一个的阈值,或一个特定值
knowledge_content, max_relevance = parse_knowledge_and_get_max_relevance(raw_knowledge_str)
logger.info(
f"{self.log_prefix} 完整窗口检索到知识,(此处未设阈值,或相关性: {max_relevance:.4f})。"
)
final_knowledge_to_add = knowledge_content # 默认采纳
retrieval_source_info = (
f"完整窗口 (最多{observation.max_now_obs_len}条, 相关性 {max_relevance:.4f})"
)
else:
logger.debug(f"{self.log_prefix} 完整窗口检索也未找到知识。")
except Exception as e_full:
logger.error(f"{self.log_prefix} 完整窗口知识检索发生错误: {e_full}")
logger.error(traceback.format_exc())
# 将最终选定的知识(如果有)添加到 structured_info
if final_knowledge_to_add:
knowledge_item = {
"type": "lpmm_knowledge",
"id": f"lpmm_knowledge_{time.time()}",
"content": final_knowledge_to_add,
"ttl": 1, # 由于是当轮精心选择的可以让TTL短一些下次重新评估或者按照您的意愿设为3
}
# 我们在方法开头已经清理了旧的 lpmm_knowledge这里直接添加新的
self.structured_info.append(knowledge_item)
logger.info(
f"{self.log_prefix} 添加了来自 '{retrieval_source_info}' 的知识到 structured_info (ID: {knowledge_item['id']})"
)
self._update_structured_info_str() # 更新字符串表示
else:
logger.info(f"{self.log_prefix} 经过所有阶梯检索后,没有最终采纳的知识。")
# ---------- 3. 准备工具和个性化数据 ----------
# 初始化工具
tool_instance = ToolUser()
@ -300,12 +477,16 @@ class SubMind:
# 思考指导选项和权重
hf_options = [
(
"可以参考之前的想法,在原来想法的基础上继续思考,但是也要注意话题的推进,不要在一个话题上停留太久,除非你觉得真的有必要",
"可以参考之前的想法,在原来想法的基础上继续思考,但是也要注意话题的推进,**不要在一个话题上停留太久或揪着一个话题不放,除非你觉得真的有必要**",
0.3,
),
("可以参考之前的想法,在原来的想法上尝试新的话题", 0.3),
("不要太深入,注意话题的推进,不要在一个话题上停留太久,除非你觉得真的有必要", 0.2),
("进行深入思考,但是注意话题的推进,不要在一个话题上停留太久,除非你觉得真的有必要", 0.2),
("可以参考之前的想法,在原来的想法上**尝试新的话题**", 0.3),
("不要太深入,注意话题的推进,**不要在一个话题上停留太久或揪着一个话题不放,除非你觉得真的有必要**", 0.2),
(
"进行深入思考,但是注意话题的推进,**不要在一个话题上停留太久或揪着一个话题不放,除非你觉得真的有必要**",
0.2,
),
("可以参考之前的想法继续思考,并结合你自身的人设,知识,信息,回忆等等", 0.08),
]
last_cycle = history_cycle[-1] if history_cycle else None

View File

@ -1,7 +1,12 @@
import asyncio
import time
from .plugins.utils.statistic import LLMStatistics
from .plugins.moods.moods import MoodManager
from maim_message import MessageServer
from .plugins.remote.remote import TelemetryHeartBeatTask
from .manager.async_task_manager import async_task_manager
from .plugins.utils.statistic import OnlineTimeRecordTask, StatisticOutputTask
from .manager.mood_manager import MoodPrintTask, MoodUpdateTask
from .plugins.schedule.schedule_generator import bot_schedule
from .plugins.emoji_system.emoji_manager import emoji_manager
from .plugins.person_info.person_info import person_info_manager
@ -14,9 +19,8 @@ from .plugins.storage.storage import MessageStorage
from .config.config import global_config
from .plugins.chat.bot import chat_bot
from .common.logger_manager import get_logger
from .plugins.remote import heartbeat_thread # noqa: F401
from .individuality.individuality import Individuality
from .common.server import global_server
from .common.server import global_server, Server
from rich.traceback import install
from .api.main import start_api_server
@ -27,17 +31,14 @@ logger = get_logger("main")
class MainSystem:
def __init__(self):
self.llm_stats = LLMStatistics("llm_statistics.txt")
self.mood_manager = MoodManager.get_instance()
self.hippocampus_manager = HippocampusManager.get_instance()
self._message_manager_started = False
self.individuality = Individuality.get_instance()
self.hippocampus_manager: HippocampusManager = HippocampusManager.get_instance()
self.individuality: Individuality = Individuality.get_instance()
# 使用消息API替代直接的FastAPI实例
from .plugins.message import global_api
self.app = global_api
self.server = global_server
self.app: MessageServer = global_api
self.server: Server = global_server
async def initialize(self):
"""初始化系统组件"""
@ -51,9 +52,15 @@ class MainSystem:
async def _init_components(self):
"""初始化其他组件"""
init_start_time = time.time()
# 启动LLM统计
self.llm_stats.start()
logger.success("LLM统计功能启动成功")
# 添加在线时间统计任务
await async_task_manager.add_task(OnlineTimeRecordTask())
# 添加统计信息输出任务
await async_task_manager.add_task(StatisticOutputTask())
# 添加遥测心跳任务
await async_task_manager.add_task(TelemetryHeartBeatTask())
# 启动API服务器
start_api_server()
@ -62,9 +69,10 @@ class MainSystem:
emoji_manager.initialize()
logger.success("表情包管理器初始化成功")
# 启动情绪管理器
self.mood_manager.start_mood_update(update_interval=global_config.mood_update_interval)
logger.success("情绪管理器启动成功")
# 添加情绪衰减任务
await async_task_manager.add_task(MoodUpdateTask())
# 添加情绪打印任务
await async_task_manager.add_task(MoodPrintTask())
# 检查并清除person_info冗余字段启动个人习惯推断
await person_info_manager.del_all_undefined_field()
@ -129,7 +137,6 @@ class MainSystem:
self.build_memory_task(),
self.forget_memory_task(),
self.consolidate_memory_task(),
self.print_mood_task(),
self.remove_recalled_message_task(),
emoji_manager.start_periodic_check_register(),
self.app.run(),
@ -163,12 +170,6 @@ class MainSystem:
await HippocampusManager.get_instance().consolidate_memory()
print("\033[1;32m[记忆整合]\033[0m 记忆整合完成")
async def print_mood_task(self):
"""打印情绪状态"""
while True:
self.mood_manager.print_mood_status()
await asyncio.sleep(60)
@staticmethod
async def remove_recalled_message_task():
"""删除撤回消息任务"""

View File

@ -0,0 +1,150 @@
from abc import abstractmethod
import asyncio
from asyncio import Task, Event, Lock
from typing import Callable, Dict
from src.common.logger_manager import get_logger
logger = get_logger("async_task_manager")
class AsyncTask:
"""异步任务基类"""
def __init__(self, task_name: str | None = None, wait_before_start: int = 0, run_interval: int = 0):
self.task_name: str = task_name or self.__class__.__name__
"""任务名称"""
self.wait_before_start: int = wait_before_start
"""运行任务前是否进行等待单位设为0则不等待"""
self.run_interval: int = run_interval
"""多次运行的时间间隔单位设为0则仅运行一次"""
@abstractmethod
async def run(self):
"""
任务的执行过程
"""
pass
async def start_task(self, abort_flag: asyncio.Event):
if self.wait_before_start > 0:
# 等待指定时间后开始任务
await asyncio.sleep(self.wait_before_start)
while not abort_flag.is_set():
await self.run()
if self.run_interval > 0:
await asyncio.sleep(self.run_interval)
else:
break
class AsyncTaskManager:
"""异步任务管理器"""
def __init__(self):
self.tasks: Dict[str, Task] = {}
"""任务列表"""
self.abort_flag: Event = Event()
"""是否中止任务标志"""
self._lock: Lock = Lock()
"""异步锁当可能出现await时需要加锁"""
def _remove_task_call_back(self, task: Task):
"""
call_back: 任务完成后移除任务
"""
task_name = task.get_name()
if task_name in self.tasks:
# 任务完成后移除任务
del self.tasks[task_name]
logger.debug(f"已移除任务 '{task_name}'")
else:
logger.warning(f"尝试移除不存在的任务 '{task_name}'")
@staticmethod
def _default_finish_call_back(task: Task):
"""
call_back: 默认的任务完成回调函数
"""
try:
task.result()
logger.debug(f"任务 '{task.get_name()}' 完成")
except asyncio.CancelledError:
logger.debug(f"任务 '{task.get_name()}' 被取消")
except Exception as e:
logger.error(f"任务 '{task.get_name()}' 执行时发生异常: {e}", exc_info=True)
async def add_task(self, task: AsyncTask, call_back: Callable[[asyncio.Task], None] | None = None):
"""
添加任务
"""
if not issubclass(task.__class__, AsyncTask):
raise TypeError(f"task '{task.__class__.__name__}' 必须是继承 AsyncTask 的子类")
async with self._lock: # 由于可能需要await等待任务完成所以需要加异步锁
if task.task_name in self.tasks:
logger.warning(f"已存在名称为 '{task.task_name}' 的任务,正在尝试取消并替换")
self.tasks[task.task_name].cancel() # 取消已存在的任务
await self.tasks[task.task_name] # 等待任务完成
logger.info(f"成功结束任务 '{task.task_name}'")
# 创建新任务
task_inst = asyncio.create_task(task.start_task(self.abort_flag))
task_inst.set_name(task.task_name)
task_inst.add_done_callback(self._remove_task_call_back) # 添加完成回调函数-完成任务后自动移除任务
task_inst.add_done_callback(
call_back or self._default_finish_call_back
) # 添加完成回调函数-用户自定义或默认的FallBack
self.tasks[task.task_name] = task_inst # 将任务添加到任务列表
logger.info(f"已启动任务 '{task.task_name}'")
def get_tasks_status(self) -> Dict[str, Dict[str, str]]:
"""
获取所有任务的状态
"""
tasks_status = {}
for task_name, task in self.tasks.items():
tasks_status[task_name] = {
"status": "running" if not task.done() else "done",
}
return tasks_status
async def stop_and_wait_all_tasks(self):
"""
终止所有任务并等待它们完成该方法会阻塞其它尝试add_task()的操作
"""
async with self._lock: # 由于可能需要await等待任务完成所以需要加异步锁
# 设置中止标志
self.abort_flag.set()
# 取消所有任务
for name, inst in self.tasks.items():
try:
inst.cancel()
except asyncio.CancelledError:
logger.info(f"已取消任务 '{name}'")
# 等待所有任务完成
for task_name, task_inst in self.tasks.items():
if not task_inst.done():
try:
await task_inst
except asyncio.CancelledError: # 此处再次捕获取消异常防止stop_all_tasks()时延迟抛出异常
logger.info(f"任务 {task_name} 已取消")
except Exception as e:
logger.error(f"任务 {task_name} 执行时发生异常: {e}", ext_info=True)
# 清空任务列表
self.tasks.clear()
self.abort_flag.clear()
logger.info("所有异步任务已停止")
async_task_manager = AsyncTaskManager()
"""全局异步任务管理器实例"""

View File

@ -0,0 +1,67 @@
import json
import os
from src.common.logger_manager import get_logger
LOCAL_STORE_FILE_PATH = "data/local_store.json"
logger = get_logger("local_storage")
class LocalStoreManager:
file_path: str
"""本地存储路径"""
store: dict[str, str | list | dict | int | float | bool]
"""本地存储数据"""
def __init__(self, local_store_path: str | None = None):
self.file_path = local_store_path or LOCAL_STORE_FILE_PATH
self.store = {}
self.load_local_store()
def __getitem__(self, item: str) -> str | list | dict | int | float | bool | None:
"""获取本地存储数据"""
return self.store.get(item, None)
def __setitem__(self, key: str, value: str | list | dict | int | float | bool):
"""设置本地存储数据"""
self.store[key] = value
self.save_local_store()
def __contains__(self, item: str) -> bool:
"""检查本地存储数据是否存在"""
return item in self.store
def load_local_store(self):
"""加载本地存储数据"""
if os.path.exists(self.file_path):
# 存在本地存储文件,加载数据
logger.info("正在阅读记事本......我在看,我真的在看!")
logger.debug(f"加载本地存储数据: {self.file_path}")
try:
with open(self.file_path, "r", encoding="utf-8") as f:
self.store = json.load(f)
logger.success("全都记起来了!")
except json.JSONDecodeError:
logger.warning("啊咧?记事本被弄脏了,正在重建记事本......")
self.store = {}
with open(self.file_path, "w", encoding="utf-8") as f:
json.dump({}, f, ensure_ascii=False, indent=4)
logger.success("记事本重建成功!")
else:
# 不存在本地存储文件,创建新的目录和文件
logger.warning("啊咧?记事本不存在,正在创建新的记事本......")
os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
with open(self.file_path, "w", encoding="utf-8") as f:
json.dump({}, f, ensure_ascii=False, indent=4)
logger.success("记事本创建成功!")
def save_local_store(self):
"""保存本地存储数据"""
logger.debug(f"保存本地存储数据: {self.file_path}")
with open(self.file_path, "w", encoding="utf-8") as f:
json.dump(self.store, f, ensure_ascii=False, indent=4)
local_storage = LocalStoreManager("data/local_store.json") # 全局单例化

View File

@ -0,0 +1,296 @@
import asyncio
import math
import time
from dataclasses import dataclass
from typing import Dict, Tuple
from ..config.config import global_config
from ..common.logger_manager import get_logger
from ..manager.async_task_manager import AsyncTask
from ..individuality.individuality import Individuality
logger = get_logger("mood")
@dataclass
class MoodState:
valence: float
"""愉悦度 (-1.0 到 1.0)-1表示极度负面1表示极度正面"""
arousal: float
"""唤醒度 (-1.0 到 1.0)-1表示抑制1表示兴奋"""
text: str
"""心情的文本描述"""
@dataclass
class MoodChangeHistory:
valence_direction_factor: int
"""愉悦度变化的系数(正为增益,负为抑制)"""
arousal_direction_factor: int
"""唤醒度变化的系数(正为增益,负为抑制)"""
class MoodUpdateTask(AsyncTask):
def __init__(self):
super().__init__(
task_name="Mood Update Task",
wait_before_start=global_config.mood_update_interval,
run_interval=global_config.mood_update_interval,
)
# 从配置文件获取衰减率
self.decay_rate_valence: float = 1 - global_config.mood_decay_rate
"""愉悦度衰减率"""
self.decay_rate_arousal: float = 1 - global_config.mood_decay_rate
"""唤醒度衰减率"""
self.last_update = time.time()
"""上次更新时间"""
async def run(self):
current_time = time.time()
time_diff = current_time - self.last_update
agreeableness_factor = 1 # 宜人性系数
agreeableness_bias = 0 # 宜人性偏置
neuroticism_factor = 0.5 # 神经质系数
# 获取人格特质
personality = Individuality.get_instance().personality
if personality:
# 神经质:影响情绪变化速度
neuroticism_factor = 1 + (personality.neuroticism - 0.5) * 0.4
agreeableness_factor = 1 + (personality.agreeableness - 0.5) * 0.4
# 宜人性:影响情绪基准线
if personality.agreeableness < 0.2:
agreeableness_bias = (personality.agreeableness - 0.2) * 0.5
elif personality.agreeableness > 0.8:
agreeableness_bias = (personality.agreeableness - 0.8) * 0.5
else:
agreeableness_bias = 0
# 分别计算正向和负向的衰减率
if mood_manager.current_mood.valence >= 0:
# 正向情绪衰减
decay_rate_positive = self.decay_rate_valence * (1 / agreeableness_factor)
valence_target = 0 + agreeableness_bias
new_valence = valence_target + (mood_manager.current_mood.valence - valence_target) * math.exp(
-decay_rate_positive * time_diff * neuroticism_factor
)
else:
# 负向情绪衰减
decay_rate_negative = self.decay_rate_valence * agreeableness_factor
valence_target = 0 + agreeableness_bias
new_valence = valence_target + (mood_manager.current_mood.valence - valence_target) * math.exp(
-decay_rate_negative * time_diff * neuroticism_factor
)
# Arousal 向中性0回归
arousal_target = 0
new_arousal = arousal_target + (mood_manager.current_mood.arousal - arousal_target) * math.exp(
-self.decay_rate_arousal * time_diff * neuroticism_factor
)
mood_manager.set_current_mood(new_valence, new_arousal)
self.last_update = current_time
class MoodPrintTask(AsyncTask):
def __init__(self):
super().__init__(
task_name="Mood Print Task",
wait_before_start=60,
run_interval=60,
)
async def run(self):
# 打印当前心情
logger.info(
f"愉悦度: {mood_manager.current_mood.valence:.2f}, "
f"唤醒度: {mood_manager.current_mood.arousal:.2f}, "
f"心情: {mood_manager.current_mood.text}"
)
class MoodManager:
# TODO: 改进,使用具有实验支持的新情绪模型
EMOTION_FACTOR_MAP: Dict[str, Tuple[float, float]] = {
"开心": (0.21, 0.6),
"害羞": (0.15, 0.2),
"愤怒": (-0.24, 0.8),
"恐惧": (-0.21, 0.7),
"悲伤": (-0.21, 0.3),
"厌恶": (-0.12, 0.4),
"惊讶": (0.06, 0.7),
"困惑": (0.0, 0.6),
"平静": (0.03, 0.5),
}
"""
情绪词映射表 {mood: (valence, arousal)}
将情绪描述词映射到愉悦度和唤醒度的元组
"""
EMOTION_POINT_MAP: Dict[Tuple[float, float], str] = {
# 第一象限:高唤醒,正愉悦
(0.5, 0.4): "兴奋",
(0.3, 0.6): "快乐",
(0.2, 0.3): "满足",
# 第二象限:高唤醒,负愉悦
(-0.5, 0.4): "愤怒",
(-0.3, 0.6): "焦虑",
(-0.2, 0.3): "烦躁",
# 第三象限:低唤醒,负愉悦
(-0.5, -0.4): "悲伤",
(-0.3, -0.3): "疲倦",
(-0.4, -0.7): "疲倦",
# 第四象限:低唤醒,正愉悦
(0.2, -0.1): "平静",
(0.3, -0.2): "安宁",
(0.5, -0.4): "放松",
}
"""
情绪文本映射表 {(valence, arousal): mood}
将量化的情绪状态元组映射到文本描述
"""
def __init__(self):
self.current_mood = MoodState(
valence=0.0,
arousal=0.0,
text="平静",
)
"""当前情绪状态"""
self.mood_change_history: MoodChangeHistory = MoodChangeHistory(
valence_direction_factor=0,
arousal_direction_factor=0,
)
"""情绪变化历史"""
self._lock = asyncio.Lock()
"""异步锁,用于保护线程安全"""
def set_current_mood(self, new_valence: float, new_arousal: float):
"""
设置当前情绪状态
:param new_valence: 新的愉悦度
:param new_arousal: 新的唤醒度
"""
# 限制范围
self.current_mood.valence = max(-1.0, min(new_valence, 1.0))
self.current_mood.arousal = max(-1.0, min(new_arousal, 1.0))
closest_mood = None
min_distance = float("inf")
for (v, a), text in self.EMOTION_POINT_MAP.items():
# 计算当前情绪状态与每个情绪文本的欧氏距离
distance = math.sqrt((self.current_mood.valence - v) ** 2 + (self.current_mood.arousal - a) ** 2)
if distance < min_distance:
min_distance = distance
closest_mood = text
if closest_mood:
self.current_mood.text = closest_mood
def update_current_mood(self, valence_delta: float, arousal_delta: float):
"""
根据愉悦度和唤醒度变化量更新当前情绪状态
:param valence_delta: 愉悦度变化量
:param arousal_delta: 唤醒度变化量
"""
# 计算连续增益/抑制
# 规则:多次相同方向的变化会有更大的影响系数,反方向的变化会清零影响系数(系数的正负号由变化方向决定)
if valence_delta * self.mood_change_history.valence_direction_factor > 0:
# 如果方向相同,则根据变化方向改变系数
if valence_delta > 0:
self.mood_change_history.valence_direction_factor += 1 # 若为正向,则增加
else:
self.mood_change_history.valence_direction_factor -= 1 # 若为负向,则减少
else:
# 如果方向不同,则重置计数
self.mood_change_history.valence_direction_factor = 0
if arousal_delta * self.mood_change_history.arousal_direction_factor > 0:
# 如果方向相同,则根据变化方向改变系数
if arousal_delta > 0:
self.mood_change_history.arousal_direction_factor += 1 # 若为正向,则增加计数
else:
self.mood_change_history.arousal_direction_factor -= 1 # 若为负向,则减少计数
else:
# 如果方向不同,则重置计数
self.mood_change_history.arousal_direction_factor = 0
# 计算增益/抑制的结果
# 规则:如果当前情绪状态与变化方向相同,则增益;否则抑制
if self.current_mood.valence * self.mood_change_history.valence_direction_factor > 0:
valence_delta = valence_delta * (1.01 ** abs(self.mood_change_history.valence_direction_factor))
else:
valence_delta = valence_delta * (0.99 ** abs(self.mood_change_history.valence_direction_factor))
if self.current_mood.arousal * self.mood_change_history.arousal_direction_factor > 0:
arousal_delta = arousal_delta * (1.01 ** abs(self.mood_change_history.arousal_direction_factor))
else:
arousal_delta = arousal_delta * (0.99 ** abs(self.mood_change_history.arousal_direction_factor))
self.set_current_mood(
new_valence=self.current_mood.valence + valence_delta,
new_arousal=self.current_mood.arousal + arousal_delta,
)
def get_mood_prompt(self) -> str:
"""
根据当前情绪状态生成提示词
"""
base_prompt = f"当前心情:{self.current_mood.text}"
# 根据情绪状态添加额外的提示信息
if self.current_mood.valence > 0.5:
base_prompt += "你现在心情很好,"
elif self.current_mood.valence < -0.5:
base_prompt += "你现在心情不太好,"
if self.current_mood.arousal > 0.4:
base_prompt += "情绪比较激动。"
elif self.current_mood.arousal < -0.4:
base_prompt += "情绪比较平静。"
return base_prompt
def get_arousal_multiplier(self) -> float:
"""
根据当前情绪状态返回唤醒度乘数
"""
if self.current_mood.arousal > 0.4:
multiplier = 1 + min(0.15, (self.current_mood.arousal - 0.4) / 3)
return multiplier
elif self.current_mood.arousal < -0.4:
multiplier = 1 - min(0.15, ((0 - self.current_mood.arousal) - 0.4) / 3)
return multiplier
return 1.0
def update_mood_from_emotion(self, emotion: str, intensity: float = 1.0) -> None:
"""
根据情绪词更新心情状态
:param emotion: 情绪词'开心', '悲伤'等位于self.EMOTION_FACTOR_MAP中的键
:param intensity: 情绪强度0.0-1.0
"""
if emotion not in self.EMOTION_FACTOR_MAP:
logger.error(f"[情绪更新] 未知情绪词: {emotion}")
return
valence_change, arousal_change = self.EMOTION_FACTOR_MAP[emotion]
old_valence = self.current_mood.valence
old_arousal = self.current_mood.arousal
old_mood = self.current_mood.text
self.update_current_mood(valence_change, arousal_change) # 更新当前情绪状态
logger.info(
f"[情绪变化] {emotion}(强度:{intensity:.2f}) | 愉悦度:{old_valence:.2f}->{self.current_mood.valence:.2f}, 唤醒度:{old_arousal:.2f}->{self.current_mood.arousal:.2f} | 心情:{old_mood}->{self.current_mood.text}"
)
mood_manager = MoodManager()
"""全局情绪管理器"""

View File

@ -1,3 +1,5 @@
# TODO: 开机自启,遍历所有可发起的聊天流,而不是等待 PFC 实例结束
# TODO: 优化 idle 逻辑 增强其与 PFC 模式的联动
from typing import Optional, Dict, Set
import asyncio
import time

View File

@ -11,7 +11,7 @@ from rich.traceback import install
install(extra_lines=3)
logger = get_module_logger("chat_observer")
logger = get_module_logger("pfc_chat_observer")
class ChatObserver:

View File

@ -10,7 +10,7 @@ from ..chat.message import Message # 假设 Message 类型被 _convert_to_messa
from src.config.config import global_config
from ..person_info.person_info import person_info_manager
from ..person_info.relationship_manager import relationship_manager
from ..moods.moods import MoodManager
from src.manager.mood_manager import mood_manager
from .pfc_relationship import PfcRelationshipUpdater, PfcRepationshipTranslator
from .pfc_emotion import PfcEmotionUpdater
@ -57,7 +57,7 @@ class Conversation:
self.person_info_mng = person_info_manager
self.relationship_mng = relationship_manager
self.mood_mng = MoodManager.get_instance()
self.mood_mng = mood_manager
self.relationship_updater: Optional[PfcRelationshipUpdater] = None
self.relationship_translator: Optional[PfcRepationshipTranslator] = None
@ -138,9 +138,6 @@ class Conversation:
logger.debug(f"[私聊][{self.private_name}] 已减少IdleChat活跃实例计数")
if self.observation_info and self.chat_observer: # 确保二者都存在
self.observation_info.unbind_from_chat_observer() # 解绑
if self.mood_mng and hasattr(self.mood_mng, "stop_mood_update") and self.mood_mng._running: # type: ignore
self.mood_mng.stop_mood_update() # type: ignore
logger.debug(f"[私聊][{self.private_name}] MoodManager 后台更新已停止。")
self._initialized = False # 标记为未初始化
logger.info(f"[私聊][{self.private_name}] 对话实例 {self.stream_id} 已停止。")

View File

@ -249,22 +249,6 @@ async def initialize_core_components(conversation_instance: "Conversation"):
# 不需要再次启动,只需确保已初始化
logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) IdleChat实例已初始化")
if (
conversation_instance.mood_mng
and hasattr(conversation_instance.mood_mng, "start_mood_update")
and not conversation_instance.mood_mng._running
): # type: ignore
conversation_instance.mood_mng.start_mood_update(update_interval=global_config.mood_update_interval) # type: ignore
logger.debug(
f"[私聊][{conversation_instance.private_name}] (Initializer) MoodManager 已启动后台更新,间隔: {global_config.mood_update_interval} 秒。"
)
elif conversation_instance.mood_mng and conversation_instance.mood_mng._running: # type: ignore
logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) MoodManager 已在运行中。")
else:
logger.warning(
f"[私聊][{conversation_instance.private_name}] (Initializer) MoodManager 未能启动,相关功能可能受限。"
)
if (
conversation_instance.conversation_info
and conversation_instance.conversation_info.person_id
@ -299,7 +283,7 @@ async def initialize_core_components(conversation_instance: "Conversation"):
if conversation_instance.conversation_info and conversation_instance.mood_mng: # 确保都存在
try:
conversation_instance.conversation_info.current_emotion_text = (
conversation_instance.mood_mng.get_prompt()
conversation_instance.mood_mng.get_mood_prompt()
) # type: ignore
logger.debug(
f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化时加载情绪文本: {conversation_instance.conversation_info.current_emotion_text}"

View File

@ -118,7 +118,7 @@ async def run_conversation_loop(conversation_instance: "Conversation"):
conversation_instance.conversation_info.relationship_text = "你们的关系是:普通。"
if conversation_instance.mood_mng:
conversation_instance.conversation_info.current_emotion_text = (
conversation_instance.mood_mng.get_prompt()
conversation_instance.mood_mng.get_mood_prompt()
)
if not all(

View File

@ -12,7 +12,7 @@ from rich.traceback import install
install(extra_lines=3)
logger = get_module_logger("message_sender")
logger = get_module_logger("pfc_sender")
class DirectMessageSender:

View File

@ -11,7 +11,7 @@ from src.config.config import global_config
from .chat_observer import ChatObserver
from .chat_states import NotificationHandler, NotificationType, Notification
logger = get_module_logger("observation_info")
logger = get_module_logger("pfc_observation_info")
TIME_ZONE = tz.gettz(global_config.TIME_ZONE if global_config else "Asia/Shanghai") # 使用配置的时区,提供默认值

View File

@ -3,7 +3,7 @@ from typing import List, Dict, Any
from src.plugins.PFC.chat_observer import ChatObserver
from src.common.logger_manager import get_logger
from src.plugins.models.utils_model import LLMRequest
from src.plugins.moods.moods import MoodManager # MoodManager 本身是单例
from src.manager.mood_manager import mood_manager
from src.plugins.utils.chat_message_builder import build_readable_messages
from src.plugins.PFC.observation_info import ObservationInfo
from src.plugins.PFC.conversation_info import ConversationInfo
@ -19,7 +19,7 @@ class PfcEmotionUpdater:
"""
self.private_name = private_name
self.bot_name = bot_name
self.mood_mng = MoodManager.get_instance() # 获取 MoodManager 单例
self.mood_mng = mood_manager
# LLM 实例 (根据 global_config.llm_summary 配置)
llm_config_summary = getattr(global_config, "llm_summary", None)
@ -51,7 +51,7 @@ class PfcEmotionUpdater:
logger.error(f"[私聊][{self.private_name}] LLM未初始化无法进行情绪更新。")
# 即使LLM失败也应该更新conversation_info中的情绪文本为MoodManager的当前状态
if conversation_info and self.mood_mng:
conversation_info.current_emotion_text = self.mood_mng.get_prompt()
conversation_info.current_emotion_text = self.mood_mng.get_mood_prompt()
return
if not self.mood_mng or not conversation_info or not observation_info:
@ -97,7 +97,7 @@ class PfcEmotionUpdater:
if (
detected_emotion_word
and detected_emotion_word != "无变化"
and detected_emotion_word in self.mood_mng.emotion_map
and detected_emotion_word in self.mood_mng.EMOTION_FACTOR_MAP
):
self.mood_mng.update_mood_from_emotion(detected_emotion_word, intensity=self.EMOTION_UPDATE_INTENSITY)
logger.debug(
@ -114,4 +114,4 @@ class PfcEmotionUpdater:
# 无论LLM判断如何都更新conversation_info中的情绪文本以供Prompt使用
if conversation_info and self.mood_mng: # 确保conversation_info有效
conversation_info.current_emotion_text = self.mood_mng.get_prompt()
conversation_info.current_emotion_text = self.mood_mng.get_mood_prompt()

View File

@ -1,3 +1,4 @@
# TODO: 人格侧写(不要把人格侧写的功能实现写到这里!新建文件去)
import traceback
import re
from typing import Any

View File

@ -669,7 +669,7 @@ async def build_chat_history_text(observation_info: ObservationInfo, private_nam
if hasattr(observation_info, "chat_history_str") and observation_info.chat_history_str:
chat_history_text = observation_info.chat_history_str
elif hasattr(observation_info, "chat_history") and observation_info.chat_history:
history_slice = observation_info.chat_history[-20:]
history_slice = observation_info.chat_history[-global_config.pfc_recent_history_display_count :]
chat_history_text = await build_readable_messages(
history_slice, replace_bot_name=True, merge_messages=False, timestamp_mode="relative", read_mark=0.0
)

View File

@ -4,7 +4,7 @@ from src.config.config import global_config # 为了获取 BOT_QQ
from .chat_observer import ChatObserver
import re
logger = get_module_logger("reply_checker")
logger = get_module_logger("pfc_checker")
class ReplyChecker:

View File

@ -12,7 +12,7 @@ from .observation_info import ObservationInfo
from .conversation_info import ConversationInfo
from .pfc_utils import build_chat_history_text
logger = get_logger("reply_generator")
logger = get_logger("pfc_reply")
PROMPT_GER_VARIATIONS = [
("不用输出或提及对方的网名或绰号", 0.50),

View File

@ -5,7 +5,7 @@ from ...config.config import global_config
import time
import asyncio
logger = get_module_logger("waiter")
logger = get_module_logger("pfc_waiter")
# --- 在这里设定你想要的超时时间(秒) ---
# 例如: 120 秒 = 2 分钟

View File

@ -6,7 +6,6 @@ MaiMBot插件系统
from .chat.chat_stream import chat_manager
from .emoji_system.emoji_manager import emoji_manager
from .person_info.relationship_manager import relationship_manager
from .moods.moods import MoodManager
from .willing.willing_manager import willing_manager
from .schedule.schedule_generator import bot_schedule
@ -15,7 +14,6 @@ __all__ = [
"chat_manager",
"emoji_manager",
"relationship_manager",
"MoodManager",
"willing_manager",
"bot_schedule",
]

View File

@ -1,14 +1,13 @@
import traceback
from typing import Dict, Any
from ..moods.moods import MoodManager # 导入情绪管理器
from ...config.config import global_config
from .message import MessageRecv
from src.common.logger_manager import get_logger
from src.manager.mood_manager import mood_manager # 导入情绪管理器
from .message import MessageRecv
from ..heartFC_chat.heartflow_processor import HeartFCProcessor
from ..PFC.pfc_processor import PFCProcessor
from ..utils.prompt_builder import Prompt, global_prompt_manager
import traceback
from ...config.config import global_config
# 定义日志配置
@ -21,7 +20,7 @@ class ChatBot:
def __init__(self):
self.bot = None # bot 实例引用
self._started = False
self.mood_manager = MoodManager.get_instance() # 获取情绪管理器单例
self.mood_manager = mood_manager # 获取情绪管理器单例
self.heartflow_processor = HeartFCProcessor() # 新增
self.pfc_processor = PFCProcessor()
@ -67,12 +66,14 @@ class ChatBot:
logger.debug(f"用户{userinfo.user_id}被禁止回复")
return
if groupinfo is None:
if groupinfo is None and global_config.enable_friend_whitelist:
logger.trace("检测到私聊消息,检查")
# 好友黑名单拦截
if userinfo.user_id not in global_config.talk_allowed_private:
logger.debug(f"用户{userinfo.user_id}没有私聊权限")
return
elif not global_config.enable_friend_whitelist:
logger.debug("私聊白名单模式未启用,跳过私聊权限检查。")
# 群聊黑名单拦截
if groupinfo is not None and groupinfo.group_id not in global_config.talk_allowed_groups:

View File

@ -1,3 +1,4 @@
# TODO: 原生多模态支持
import time
from abc import abstractmethod
from dataclasses import dataclass

View File

@ -1,21 +1,20 @@
import random
import time
import re
import time
from collections import Counter
import jieba
import numpy as np
from src.common.logger import get_module_logger
from maim_message import UserInfo
from pymongo.errors import PyMongoError
from src.common.logger import get_module_logger
from src.manager.mood_manager import mood_manager
from .message import MessageRecv
from ..models.utils_model import LLMRequest
from ..utils.typo_generator import ChineseTypoGenerator
from ...config.config import global_config
from .message import MessageRecv
from maim_message import UserInfo
from ..moods.moods import MoodManager
from ...common.database import db
from ...config.config import global_config
logger = get_module_logger("chat_utils")
@ -252,7 +251,7 @@ def split_into_sentences_w_remove_punctuation(text: str) -> list[str]:
if len_text < 12:
split_strength = 0.2
elif len_text < 32:
split_strength = 0.6
split_strength = 0.5
else:
split_strength = 0.7
# 合并概率与分割强度相反
@ -371,7 +370,7 @@ def process_llm_response(text: str) -> list[str]:
else:
sentences.append(sentence)
if len(sentences) > max_sentence_num:
if len(sentences) > (max_sentence_num * 2):
logger.warning(f"分割后消息数量过多 ({len(sentences)} 条),返回默认回复")
return [f"{global_config.BOT_NICKNAME}不知道哦"]
@ -405,7 +404,6 @@ def calculate_typing_time(
- 在所有输入结束后额外加上回车时间0.3
- 如果is_emoji为True将使用固定1秒的输入时间
"""
mood_manager = MoodManager.get_instance()
# 将0-1的唤醒度映射到-1到1
mood_arousal = mood_manager.current_mood.arousal
# 映射到0.5到2倍的速度系数
@ -419,8 +417,8 @@ def calculate_typing_time(
if chinese_chars == 1 and len(input_string.strip()) == 1:
return chinese_time * 3 + 0.3 # 加上回车时间
total_time = 0
# 正常计算所有字符的输入时间
total_time = 0.0
for char in input_string:
if "\u4e00" <= char <= "\u9fff": # 判断是否为中文字符
total_time += chinese_time
@ -617,7 +615,7 @@ def translate_timestamp_to_human_readable(timestamp: float, mode: str = "normal"
str: 格式化后的时间字符串
"""
if mode == "normal":
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp))
return time.strftime("%Y-%m-%d %H:%M:%S ", time.localtime(timestamp))
elif mode == "relative":
now = time.time()
diff = now - timestamp

View File

@ -144,7 +144,7 @@ class NicknameManager:
self.nickname_queue: asyncio.Queue = asyncio.Queue(maxsize=self.queue_max_size)
self._stop_event = threading.Event() # stop_event 仍然使用 threading.Event因为它是由另一个线程设置的
self._nickname_thread: Optional[threading.Thread] = None
self.sleep_interval = getattr(self.config, "nickname_process_sleep_interval", 60) # 超时时间
self.sleep_interval = getattr(self.config, "nickname_process_sleep_interval", 5) # 超时时间
self._initialized = True
logger.info("NicknameManager 初始化完成。")
@ -177,6 +177,7 @@ class NicknameManager:
self._stop_event.set() # 设置停止事件_processing_loop 会检测到
try:
# 不需要清空 asyncio.Queue让循环自然结束或被取消
# self.empty_queue(self.nickname_queue)
self._nickname_thread.join(timeout=10) # 等待线程结束
if self._nickname_thread.is_alive():
logger.warning("绰号处理器线程在超时后仍未停止。")
@ -189,6 +190,13 @@ class NicknameManager:
else:
logger.info("绰号处理器线程未在运行或已被清理。")
# def empty_queue(self, q: asyncio.Queue):
# while not q.empty():
# # Depending on your program, you may want to
# # catch QueueEmpty
# q.get_nowait()
# q.task_done()
async def trigger_nickname_analysis(
self,
anchor_message: MessageRecv,
@ -202,7 +210,7 @@ class NicknameManager:
if not self.is_enabled:
return
if random.random() > 0.9:
if random.random() < global_config.nickname_analysis_probability:
logger.debug("跳过绰号分析:随机概率未命中。")
return
@ -358,7 +366,6 @@ class NicknameManager:
logger.info(f"{log_prefix} LLM 找到绰号映射,准备更新数据库: {nickname_map_to_update}")
for user_id_str, nickname in nickname_map_to_update.items():
# ... (验证和数据库更新逻辑保持不变) ...
if not user_id_str or not nickname:
logger.warning(f"{log_prefix} 跳过无效条目: user_id='{user_id_str}', nickname='{nickname}'")
continue
@ -394,7 +401,6 @@ class NicknameManager:
"""
内部方法调用 LLM 分析聊天记录和 Bot 回复提取可靠的 用户ID-绰号 映射
"""
# ... (此方法内部逻辑保持不变) ...
if not self.llm_mapper:
logger.error("LLM 映射器未初始化,无法执行分析。")
return {"is_exist": False}

View File

@ -1,34 +1,36 @@
import asyncio
import contextlib
import json # <--- 确保导入 json
import random # <--- 添加导入
import time
import re
import traceback
import random
import json
from typing import List, Optional, Dict, Any, Deque, Callable, Coroutine
from collections import deque
from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending
from src.plugins.chat.message import Seg
from src.plugins.chat.chat_stream import ChatStream
from src.plugins.chat.message import UserInfo
from src.plugins.chat.chat_stream import chat_manager
from src.common.logger_manager import get_logger
from src.plugins.models.utils_model import LLMRequest
from src.config.config import global_config
from src.plugins.chat.utils_image import image_path_to_base64
from src.plugins.utils.timer_calculator import Timer
from src.plugins.emoji_system.emoji_manager import emoji_manager
from src.heart_flow.sub_mind import SubMind
from src.heart_flow.observation import Observation
from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_manager, prompt_builder
import contextlib
from src.plugins.utils.chat_message_builder import num_new_messages_since
from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo
from .heartFC_sender import HeartFCSender
from src.plugins.chat.utils import process_llm_response
from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
from src.plugins.moods.moods import MoodManager
from src.heart_flow.utils_chat import get_chat_type_and_target_info
from typing import List, Optional, Dict, Any, Deque, Callable, Coroutine
from rich.traceback import install
from src.common.logger_manager import get_logger
from src.config.config import global_config
from src.heart_flow.observation import Observation
from src.heart_flow.sub_mind import SubMind
from src.heart_flow.utils_chat import get_chat_type_and_target_info
from src.manager.mood_manager import mood_manager
from src.plugins.chat.chat_stream import ChatStream
from src.plugins.chat.chat_stream import chat_manager
from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending
from src.plugins.chat.message import Seg # Local import needed after move
from src.plugins.chat.message import UserInfo
from src.plugins.chat.utils import process_llm_response
from src.plugins.chat.utils_image import image_path_to_base64 # Local import needed after move
from src.plugins.emoji_system.emoji_manager import emoji_manager
from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo
from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_manager, prompt_builder
from src.plugins.models.utils_model import LLMRequest
from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
from src.plugins.utils.chat_message_builder import num_new_messages_since
from src.plugins.utils.timer_calculator import Timer # <--- Import Timer
from .heartFC_sender import HeartFCSender
from src.plugins.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat
from src.plugins.group_nickname.nickname_manager import nickname_manager
@ -808,37 +810,37 @@ class HeartFChatting:
actions_to_remove_temporarily = []
# --- 检查历史动作并决定临时移除动作 (逻辑保持不变) ---
lian_xu_wen_ben_hui_fu = 0
probability_roll = random.random()
for cycle in reversed(self._cycle_history):
if cycle.action_taken:
if cycle.action_type == "text_reply":
lian_xu_wen_ben_hui_fu += 1
else:
break
if len(self._cycle_history) > 0 and cycle.cycle_id <= self._cycle_history[0].cycle_id + (
len(self._cycle_history) - 4
):
break
logger.debug(f"{self.log_prefix}[Planner] 检测到连续文本回复次数: {lian_xu_wen_ben_hui_fu}")
# lian_xu_wen_ben_hui_fu = 0
# probability_roll = random.random()
# for cycle in reversed(self._cycle_history):
# if cycle.action_taken:
# if cycle.action_type == "text_reply":
# lian_xu_wen_ben_hui_fu += 1
# else:
# break
# if len(self._cycle_history) > 0 and cycle.cycle_id <= self._cycle_history[0].cycle_id + (
# len(self._cycle_history) - 4
# ):
# break
# logger.debug(f"{self.log_prefix}[Planner] 检测到连续文本回复次数: {lian_xu_wen_ben_hui_fu}")
if lian_xu_wen_ben_hui_fu >= 3:
logger.info(f"{self.log_prefix}[Planner] 连续回复 >= 3 次,强制移除 text_reply 和 emoji_reply")
actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"])
elif lian_xu_wen_ben_hui_fu == 2:
if probability_roll < 0.8:
logger.info(f"{self.log_prefix}[Planner] 连续回复 2 次80% 概率移除 text_reply 和 emoji_reply (触发)")
actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"])
else:
logger.info(
f"{self.log_prefix}[Planner] 连续回复 2 次80% 概率移除 text_reply 和 emoji_reply (未触发)"
)
elif lian_xu_wen_ben_hui_fu == 1:
if probability_roll < 0.4:
logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次40% 概率移除 text_reply (触发)")
actions_to_remove_temporarily.append("text_reply")
else:
logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次40% 概率移除 text_reply (未触发)")
# if lian_xu_wen_ben_hui_fu >= 3:
# logger.info(f"{self.log_prefix}[Planner] 连续回复 >= 3 次,强制移除 text_reply 和 emoji_reply")
# actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"])
# elif lian_xu_wen_ben_hui_fu == 2:
# if probability_roll < 0.8:
# logger.info(f"{self.log_prefix}[Planner] 连续回复 2 次80% 概率移除 text_reply 和 emoji_reply (触发)")
# actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"])
# else:
# logger.info(
# f"{self.log_prefix}[Planner] 连续回复 2 次80% 概率移除 text_reply 和 emoji_reply (未触发)"
# )
# elif lian_xu_wen_ben_hui_fu == 1:
# if probability_roll < 0.4:
# logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次40% 概率移除 text_reply (触发)")
# actions_to_remove_temporarily.append("text_reply")
# else:
# logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次40% 概率移除 text_reply (未触发)")
# --- 结束检查历史动作 ---
# 获取观察信息
@ -1312,7 +1314,7 @@ class HeartFChatting:
"""
try:
# 1. 获取情绪影响因子并调整模型温度
arousal_multiplier = MoodManager.get_instance().get_arousal_multiplier()
arousal_multiplier = mood_manager.get_arousal_multiplier()
current_temp = global_config.llm_normal["temp"] * arousal_multiplier
self.model_normal.temperature = current_temp # 动态调整温度

View File

@ -10,7 +10,7 @@ from src.plugins.person_info.relationship_manager import relationship_manager
from src.plugins.chat.utils import get_embedding
from ...common.database import db
from ..chat.utils import get_recent_group_speaker
from ..moods.moods import MoodManager
from src.manager.mood_manager import mood_manager
from ..memory_system.Hippocampus import HippocampusManager
from ..schedule.schedule_generator import bot_schedule
from ..knowledge.knowledge_lib import qa_manager
@ -29,15 +29,15 @@ def init_prompt():
{chat_target}
{chat_talking_prompt}
现在你想要回复或参与讨论\n
你是{bot_name}{prompt_personality}
你正在{chat_target_2},现在请你读读之前的聊天记录可以自然随意一些简短一些就像群聊里的真人一样注意把握聊天内容整体风格可以平和简短
你是{bot_name}你正在{chat_target_2}
看到以上聊天记录你刚刚在想
{current_mind_info}
因为上述想法你决定发言原因是{reason}
因为上述想法你决定发言
回复尽量简短一些请注意把握聊天内容{reply_style2}请一次只回复一个话题不要同时回复多个人{prompt_ger}
{reply_style1}说中文不要刻意突出自身学科背景注意只输出回复内容不要去主动讨论或评价别人发的表情包它们只是一种辅助表达方式
现在请你读读之前的聊天记录把你的想法组织成合适简短的语言然后发一条消息可以自然随意一些简短一些就像群聊里的真人一样注意把握聊天内容整体风格可以平和简短避免超出你内心想法的范围
这条消息可以尽量简短一些{reply_style2}请一次只回复一个话题不要同时回复多个人{prompt_ger}
{reply_style1}说中文不要刻意突出自身学科背景注意只输出消息内容不要去主动讨论或评价别人发的表情包它们只是一种辅助表达方式
{moderation_prompt}注意回复不要输出多余内容(包括前后缀冒号和引号括号表情包at或 @等 )""",
"heart_flow_prompt",
)
@ -46,56 +46,97 @@ def init_prompt():
"""
你有以下信息可供参考
{structured_info}
以上的息是你获取到的消息或许可以帮助你更好地回复
以上的息是你获取到的消息或许可以帮助你更好地回复
""",
"info_from_tools",
)
# Planner提示词 - 修改为要求 JSON 输出
Prompt(
"""你的名字是{bot_name},{prompt_personality}{chat_context_description}。需要基于以下信息决定如何参与对话:
{structured_info_block}
{nickname_info}
{chat_content_block}
{current_mind_block}
{cycle_info_block}
"""
<planner_task_definition>
现在{bot_name}开始在一个qq群聊中专注聊天你需要操控{bot_name}并且根据以下信息决定是否如何参与对话
</planner_task_definition>
请综合分析聊天内容和你看到的新消息参考内心想法并根据以下原则和可用动作做出决策
<contextual_information>
<identity>
<bot_name>{bot_name}</bot_name>
<group_nicknames>{nickname_info}</group_nicknames>
</identity>
发送新消息原则
1. 不发送新消息(no_reply)适用
- 话题无关/无聊/不感兴趣
- 最后一条消息是你自己发的且无人回应你
- 讨论你不懂的专业话题
- 你发送了太多消息且无人回复
<live_chat_context>
<chat_log>{chat_content_block}</chat_log>
</live_chat_context>
<internal_state>
<current_thoughts>{current_mind_block}</current_thoughts>
<recent_action_history>{cycle_info_block}</recent_action_history>
</internal_state>
</contextual_information>
2. 发送文字消息(text_reply)适用
- 有实质性内容需要表达
- 有人提到你但你还没有回应他
- 可以追加emoji_query表达情绪(emoji_query填写表情包的适用场合也就是当前场合)
- 不要追加太多表情
<decision_framework>
<guidance>
请综合分析聊天内容和你看到的新消息参考{bot_name}的内心想法并根据以下原则和可用动作灵活谨慎的做出决策需要符合正常的群聊社交节奏
</guidance>
3. 发送纯表情(emoji_reply)适用
- 适合用表情回应的场景
- 需提供明确的emoji_query
<decision_principles>
<principle_no_reply>
1. 以下情况可以不发送新消息(no_reply)
- {bot_name}的内心想法表达不想发言
- 话题似乎对{bot_name}来说无关/无聊/不感兴趣
- 现在说话不太合适了
- 最后一条消息是{bot_name}自己发的且无人回应{bot_name}同时{bot_name}也没有别的想要回复的消息
- 讨论不了解的专业话题或你不知道的梗且对{bot_name}来说似乎没那么重要
- {bot_name}发送了太多消息且无人回复
- 特殊情况{bot_name}的内心想法返回错误/无返回
</principle_no_reply>
4. 自我对话处理
- 如果最后一条消息是你自己发的而你还想继续发消息需自然衔接不要有不自然的内容重叠
- 避免重复或评价自己的发言
- 不要自己和自己聊天
<principle_text_reply>
2. 以下情况可以发送文字消息(text_reply)
- 确认内心想法显示{bot_name}想要发言且有实质内容想表达
- 同时确认现在适合发言
- 可以追加emoji_query表达情绪(emoji_query填写表情包的适用场合也就是当前场合)
- 不要追加太多表情
</principle_text_reply>
决策任务
{action_options_text}
<principle_emoji_reply>
3. 发送纯表情(emoji_reply)适用
- {bot_name}似乎想加入话题或继续讨论但是似乎又没什么实质表达内容
- 适合用表情回应的场景
- 需提供明确的emoji_query
- 群聊里除了{bot_name}以外的大家都在发表情包
</principle_emoji_reply>
你必须从上面列出的可用行动中选择一个并说明原因
你的决策必须以严格的 JSON 格式输出且仅包含 JSON 内容不要有任何其他文字或解释
JSON 结构如下包含三个字段 "action", "reasoning", "emoji_query":
{{
"action": "string", // 必须是上面提供的可用行动之一 (例如: '{example_action}')
"reasoning": "string", // 做出此决定的详细理由和思考过程说明你如何应用了回复原则
"emoji_query": "string" // 可选如果行动是 'emoji_reply'必须提供表情主题(填写表情包的适用场合)如果行动是 'text_reply' 且你想附带表情也在此提供表情主题否则留空字符串 ""遵循回复原则不要滥用
}}
请输出你的决策 JSON
<principle_dialogue_management>
4. 对话处理
- 如果最后一条消息是{bot_name}发的而你还想操控{bot_name}继续发消息请确保这是合适的例如{bot_name}确实有合适的补充或回应之前没回应的消息
- 注意话题的推进如果没有必要不要揪着一个话题不放
- 不要让{bot_name}自己和自己聊天
</principle_dialogue_management>
</decision_principles>
<available_actions>
决策任务
{action_options_text}
</available_actions>
</decision_framework>
<output_requirements>
<format_instruction>
你必须从available_actions列出的可用行动中选择一个并说明原因
你的决策必须以严格的 JSON 格式输出且仅包含 JSON 内容不要有任何其他文字或解释
JSON 结构如下包含三个字段 "action", "reasoning", "emoji_query":
</format_instruction>
<json_structure>
{{
"action": "string", // 必须是上面提供的可用行动之一 (例如: '{example_action}')
"reasoning": "string", // 做出此决定的详细理由和思考过程说明你如何应用了decision_principles
"emoji_query": "string" // 可选如果行动是 'emoji_reply'必须提供表情主题(填写表情包的适用场合)如果行动是 'text_reply' 且你想附带表情也在此提供表情主题否则留空字符串 ""遵循回复原则不要滥用
}}
</json_structure>
<final_request>
请输出你的决策 JSON
</final_request>
</output_requirements>
""",
"planner_prompt",
)
@ -221,18 +262,18 @@ async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_s
reply_styles1 = [
("给出日常且口语化的回复,平淡一些", 0.4),
("给出非常简短的回复", 0.4),
("给出缺失主语的回复,简短", 0.15),
("给出带有语病的回复,朴实平淡", 0.05),
("**给出省略主语的回复,简短**", 0.15),
("给出带有语病的回复,朴实平淡", 0.00),
]
reply_style1_chosen = random.choices(
[style[0] for style in reply_styles1], weights=[style[1] for style in reply_styles1], k=1
)[0]
reply_styles2 = [
("不要回复的太有条理,可以有个性", 0.6),
("不要回复的太有条理,可以复读", 0.15),
("不要回复的太有条理,可以有个性", 0.8),
("不要回复的太有条理,可以复读", 0.0),
("回复的认真一些", 0.2),
("可以回复单个表情符号", 0.05),
("可以回复单个表情符号", 0.00),
]
reply_style2_chosen = random.choices(
[style[0] for style in reply_styles2], weights=[style[1] for style in reply_styles2], k=1
@ -351,8 +392,7 @@ class PromptBuilder:
else:
logger.warning(f"Invalid person tuple encountered for relationship prompt: {person}")
mood_manager = MoodManager.get_instance()
mood_prompt = mood_manager.get_prompt()
mood_prompt = mood_manager.get_mood_prompt()
reply_styles1 = [
("给出日常且口语化的回复,平淡一些", 0.30),
("给出非常简短的回复", 0.30),
@ -362,8 +402,8 @@ class PromptBuilder:
[style[0] for style in reply_styles1], weights=[style[1] for style in reply_styles1], k=1
)[0]
reply_styles2 = [
("不用回复的太有条理,可以有个性", 0.7), # 60%概率
("不用回复的太有条理,可以复读", 0.05), # 15%概率
("不用回复的太有条理,可以有个性", 0.75), # 60%概率
("不用回复的太有条理,可以复读", 0.0), # 15%概率
("回复的认真一些", 0.2), # 20%概率
("可以回复单个表情符号", 0.05), # 5%概率
]

View File

@ -1,25 +1,26 @@
import time
import asyncio
import traceback
import statistics # 导入 statistics 模块
import time
import traceback
from random import random
from typing import List, Optional # 导入 Optional
from ..moods.moods import MoodManager
from ...config.config import global_config
from ..emoji_system.emoji_manager import emoji_manager
from .normal_chat_generator import NormalChatGenerator
from ..chat.message import MessageSending, MessageRecv, MessageThinking, MessageSet
from ..chat.message_sender import message_manager
from ..chat.utils_image import image_path_to_base64
from ..willing.willing_manager import willing_manager
from maim_message import UserInfo, Seg
from src.common.logger_manager import get_logger
from src.heart_flow.utils_chat import get_chat_type_and_target_info
from src.manager.mood_manager import mood_manager
from src.plugins.chat.chat_stream import ChatStream, chat_manager
from src.plugins.person_info.relationship_manager import relationship_manager
from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
from src.plugins.utils.timer_calculator import Timer
from src.heart_flow.utils_chat import get_chat_type_and_target_info
from .normal_chat_generator import NormalChatGenerator
from ..chat.message import MessageSending, MessageRecv, MessageThinking, MessageSet
from ..chat.message_sender import message_manager
from ..chat.utils_image import image_path_to_base64
from ..emoji_system.emoji_manager import emoji_manager
from ..willing.willing_manager import willing_manager
from ...config.config import global_config
from src.plugins.group_nickname.nickname_manager import nickname_manager
@ -46,7 +47,7 @@ class NormalChat:
# Other sync initializations
self.gpt = NormalChatGenerator()
self.mood_manager = MoodManager.get_instance()
self.mood_manager = mood_manager
self.start_time = time.time()
self.last_speak_time = 0
self._chat_task: Optional[asyncio.Task] = None

View File

@ -61,7 +61,7 @@ class QAManager:
for res in relation_search_res:
rel_str = self.embed_manager.relation_embedding_store.store.get(res[0]).str
print(f"找到相关关系,相似度:{(res[1] * 100):.2f}% - {rel_str}")
logger.debug(f"找到相关关系,相似度:{(res[1] * 100):.2f}% - {rel_str}")
# TODO: 使用LLM过滤三元组结果
# logger.info(f"LLM过滤三元组用时{time.time() - part_start_time:.2f}s")
@ -77,16 +77,16 @@ class QAManager:
logger.debug(f"文段检索用时:{part_end_time - part_start_time:.5f}s")
if len(relation_search_res) != 0:
logger.info("找到相关关系将使用RAG进行检索")
logger.debug("找到相关关系将使用RAG进行检索")
# 使用KG检索
part_start_time = time.perf_counter()
result, ppr_node_weights = self.kg_manager.kg_search(
relation_search_res, paragraph_search_res, self.embed_manager
)
part_end_time = time.perf_counter()
logger.info(f"RAG检索用时{part_end_time - part_start_time:.5f}s")
logger.debug(f"RAG检索用时{part_end_time - part_start_time:.5f}s")
else:
logger.info("未找到相关关系,将使用文段检索结果")
logger.debug("未找到相关关系,将使用文段检索结果")
result = paragraph_search_res
ppr_node_weights = None
@ -95,7 +95,7 @@ class QAManager:
for res in result:
raw_paragraph = self.embed_manager.paragraphs_embedding_store.store[res[0]].str
print(f"找到相关文段,相关系数:{res[1]:.8f}\n{raw_paragraph}\n\n")
logger.debug(f"找到相关文段,相关系数:{res[1]:.8f}\n{raw_paragraph}\n\n")
return result, ppr_node_weights
else:

View File

@ -1,293 +0,0 @@
import math
import threading
import time
from dataclasses import dataclass
from ...config.config import global_config
from src.common.logger_manager import get_logger
from ..person_info.relationship_manager import relationship_manager
from src.individuality.individuality import Individuality
logger = get_logger("mood")
@dataclass
class MoodState:
valence: float # 愉悦度 (-1.0 到 1.0)-1表示极度负面1表示极度正面
arousal: float # 唤醒度 (-1.0 到 1.0)-1表示抑制1表示兴奋
text: str # 心情文本描述
class MoodManager:
_instance = None
_lock = threading.Lock()
def __new__(cls):
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
# 确保初始化代码只运行一次
if self._initialized:
return
self._initialized = True
# 初始化心情状态
self.current_mood = MoodState(valence=0.0, arousal=0.0, text="平静")
# 从配置文件获取衰减率
self.decay_rate_valence = 1 - global_config.mood_decay_rate # 愉悦度衰减率
self.decay_rate_arousal = 1 - global_config.mood_decay_rate # 唤醒度衰减率
# 上次更新时间
self.last_update = time.time()
# 线程控制
self._running = False
self._update_thread = None
# 情绪词映射表 (valence, arousal)
self.emotion_map = {
"开心": (0.21, 0.6),
"害羞": (0.15, 0.2),
"愤怒": (-0.24, 0.8),
"恐惧": (-0.21, 0.7),
"悲伤": (-0.21, 0.3),
"厌恶": (-0.12, 0.4),
"惊讶": (0.06, 0.7),
"困惑": (0.0, 0.6),
"平静": (0.03, 0.5),
}
# 情绪文本映射表
self.mood_text_map = {
# 第一象限:高唤醒,正愉悦
(0.5, 0.4): "兴奋",
(0.3, 0.6): "快乐",
(0.2, 0.3): "满足",
# 第二象限:高唤醒,负愉悦
(-0.5, 0.4): "愤怒",
(-0.3, 0.6): "焦虑",
(-0.2, 0.3): "烦躁",
# 第三象限:低唤醒,负愉悦
(-0.5, -0.4): "悲伤",
(-0.3, -0.3): "疲倦",
(-0.4, -0.7): "疲倦",
# 第四象限:低唤醒,正愉悦
(0.2, -0.1): "平静",
(0.3, -0.2): "安宁",
(0.5, -0.4): "放松",
}
@classmethod
def get_instance(cls) -> "MoodManager":
"""获取MoodManager的单例实例"""
if cls._instance is None:
cls._instance = MoodManager()
return cls._instance
def start_mood_update(self, update_interval: float = 5.0) -> None:
"""
启动情绪更新线程
:param update_interval: 更新间隔
"""
if self._running:
return
self._running = True
self._update_thread = threading.Thread(
target=self._continuous_mood_update, args=(update_interval,), daemon=True
)
self._update_thread.start()
def stop_mood_update(self) -> None:
"""停止情绪更新线程"""
self._running = False
if self._update_thread and self._update_thread.is_alive():
self._update_thread.join()
def _continuous_mood_update(self, update_interval: float) -> None:
"""
持续更新情绪状态的线程函数
:param update_interval: 更新间隔
"""
while self._running:
self._apply_decay()
self._update_mood_text()
time.sleep(update_interval)
def _apply_decay(self) -> None:
"""应用情绪衰减,正向和负向情绪分开计算"""
current_time = time.time()
time_diff = current_time - self.last_update
agreeableness_factor = 1
agreeableness_bias = 0
neuroticism_factor = 0.5
# 获取人格特质
personality = Individuality.get_instance().personality
if personality:
# 神经质:影响情绪变化速度
neuroticism_factor = 1 + (personality.neuroticism - 0.5) * 0.4
agreeableness_factor = 1 + (personality.agreeableness - 0.5) * 0.4
# 宜人性:影响情绪基准线
if personality.agreeableness < 0.2:
agreeableness_bias = (personality.agreeableness - 0.2) * 0.5
elif personality.agreeableness > 0.8:
agreeableness_bias = (personality.agreeableness - 0.8) * 0.5
else:
agreeableness_bias = 0
# 分别计算正向和负向的衰减率
if self.current_mood.valence >= 0:
# 正向情绪衰减
decay_rate_positive = self.decay_rate_valence * (1 / agreeableness_factor)
valence_target = 0 + agreeableness_bias
self.current_mood.valence = valence_target + (self.current_mood.valence - valence_target) * math.exp(
-decay_rate_positive * time_diff * neuroticism_factor
)
else:
# 负向情绪衰减
decay_rate_negative = self.decay_rate_valence * agreeableness_factor
valence_target = 0 + agreeableness_bias
self.current_mood.valence = valence_target + (self.current_mood.valence - valence_target) * math.exp(
-decay_rate_negative * time_diff * neuroticism_factor
)
# Arousal 向中性0回归
arousal_target = 0
self.current_mood.arousal = arousal_target + (self.current_mood.arousal - arousal_target) * math.exp(
-self.decay_rate_arousal * time_diff * neuroticism_factor
)
# 确保值在合理范围内
self.current_mood.valence = max(-1.0, min(1.0, self.current_mood.valence))
self.current_mood.arousal = max(-1.0, min(1.0, self.current_mood.arousal))
self.last_update = current_time
def update_mood_from_text(self, text: str, valence_change: float, arousal_change: float) -> None:
"""根据输入文本更新情绪状态"""
self.current_mood.valence += valence_change
self.current_mood.arousal += arousal_change
# 限制范围
self.current_mood.valence = max(-1.0, min(1.0, self.current_mood.valence))
self.current_mood.arousal = max(-1.0, min(1.0, self.current_mood.arousal))
self._update_mood_text()
def set_mood_text(self, text: str) -> None:
"""直接设置心情文本"""
self.current_mood.text = text
def _update_mood_text(self) -> None:
"""根据当前情绪状态更新文本描述"""
closest_mood = None
min_distance = float("inf")
for (v, a), text in self.mood_text_map.items():
distance = math.sqrt((self.current_mood.valence - v) ** 2 + (self.current_mood.arousal - a) ** 2)
if distance < min_distance:
min_distance = distance
closest_mood = text
if closest_mood:
self.current_mood.text = closest_mood
def update_mood_by_user(self, user_id: str, valence_change: float, arousal_change: float) -> None:
"""根据用户ID更新情绪状态"""
# 这里可以根据用户ID添加特定的权重或规则
weight = 1.0 # 默认权重
self.current_mood.valence += valence_change * weight
self.current_mood.arousal += arousal_change * weight
# 限制范围
self.current_mood.valence = max(-1.0, min(1.0, self.current_mood.valence))
self.current_mood.arousal = max(-1.0, min(1.0, self.current_mood.arousal))
self._update_mood_text()
def get_prompt(self) -> str:
"""根据当前情绪状态生成提示词"""
base_prompt = f"当前心情:{self.current_mood.text}"
# 根据情绪状态添加额外的提示信息
if self.current_mood.valence > 0.5:
base_prompt += "你现在心情很好,"
elif self.current_mood.valence < -0.5:
base_prompt += "你现在心情不太好,"
if self.current_mood.arousal > 0.4:
base_prompt += "情绪比较激动。"
elif self.current_mood.arousal < -0.4:
base_prompt += "情绪比较平静。"
return base_prompt
def get_arousal_multiplier(self) -> float:
"""根据当前情绪状态返回唤醒度乘数"""
if self.current_mood.arousal > 0.4:
multiplier = 1 + min(0.15, (self.current_mood.arousal - 0.4) / 3)
return multiplier
elif self.current_mood.arousal < -0.4:
multiplier = 1 - min(0.15, ((0 - self.current_mood.arousal) - 0.4) / 3)
return multiplier
return 1.0
def get_current_mood(self) -> MoodState:
"""获取当前情绪状态"""
return self.current_mood
def print_mood_status(self) -> None:
"""打印当前情绪状态"""
logger.info(
f"愉悦度: {self.current_mood.valence:.2f}, "
f"唤醒度: {self.current_mood.arousal:.2f}, "
f"心情: {self.current_mood.text}"
)
def update_mood_from_emotion(self, emotion: str, intensity: float = 1.0) -> None:
"""
根据情绪词更新心情状态
:param emotion: 情绪词'happy', 'sad'
:param intensity: 情绪强度0.0-1.0
"""
if emotion not in self.emotion_map:
logger.debug(f"[情绪更新] 未知情绪词: {emotion}")
return
valence_change, arousal_change = self.emotion_map[emotion]
old_valence = self.current_mood.valence
old_arousal = self.current_mood.arousal
old_mood = self.current_mood.text
valence_change = relationship_manager.feedback_to_mood(valence_change)
# 应用情绪强度
valence_change *= intensity
arousal_change *= intensity
# 更新当前情绪状态
self.current_mood.valence += valence_change
self.current_mood.arousal += arousal_change
# 限制范围
self.current_mood.valence = max(-1.0, min(1.0, self.current_mood.valence))
self.current_mood.arousal = max(-1.0, min(1.0, self.current_mood.arousal))
self._update_mood_text()
logger.info(
f"[情绪变化] {emotion}(强度:{intensity:.2f}) | 愉悦度:{old_valence:.2f}->{self.current_mood.valence:.2f}, 唤醒度:{old_arousal:.2f}->{self.current_mood.arousal:.2f} | 心情:{old_mood}->{self.current_mood.text}"
)

View File

@ -8,6 +8,9 @@ import random
from typing import List, Dict
from ...common.database import db
from maim_message import UserInfo
from ...manager.mood_manager import mood_manager
# import re
# import traceback
@ -24,9 +27,7 @@ class RelationshipManager:
@property
def mood_manager(self):
if self._mood_manager is None:
from ..moods.moods import MoodManager # 延迟导入
self._mood_manager = MoodManager.get_instance()
self._mood_manager = mood_manager
return self._mood_manager
def positive_feedback_sys(self, label: str, stance: str):
@ -62,9 +63,7 @@ class RelationshipManager:
def mood_feedback(self, value):
"""情绪反馈"""
mood_manager = self.mood_manager
mood_gain = mood_manager.get_current_mood().valence ** 2 * math.copysign(
1, value * mood_manager.get_current_mood().valence
)
mood_gain = mood_manager.current_mood.valence**2 * math.copysign(1, value * mood_manager.current_mood.valence)
value += value * mood_gain
logger.info(f"当前relationship增益系数{mood_gain:.3f}")
return value

View File

@ -1,4 +0,0 @@
from .remote import main
# 启动心跳线程
heartbeat_thread = main()

View File

@ -1,248 +1,142 @@
import asyncio
import requests
import time
import uuid
import platform
import os
import json
import threading
import subprocess
# from loguru import logger
from src.common.logger_manager import get_logger
from src.config.config import global_config
from src.manager.async_task_manager import AsyncTask
from src.manager.local_store_manager import local_storage
logger = get_logger("remote")
# --- 使用向上导航的方式定义路径 ---
# 1. 获取当前文件 (remote.py) 所在的目录
current_dir = os.path.dirname(os.path.abspath(__file__))
# 2. 从当前目录向上导航三级找到项目根目录
# (src/plugins/remote/ -> src/plugins/ -> src/ -> project_root)
root_dir = os.path.abspath(os.path.join(current_dir, "..", "..", ".."))
# 3. 定义 data 目录的路径 (位于项目根目录下)
data_dir = os.path.join(root_dir, "data")
# 4. 定义 UUID 文件在 data 目录下的完整路径
UUID_FILE = os.path.join(data_dir, "client_uuid.json")
# --- 路径定义结束 ---
TELEMETRY_SERVER_URL = "http://localhost:8080"
"""遥测服务地址"""
# 生成或获取客户端唯一ID
def get_unique_id():
# --- 在尝试读写 UUID_FILE 之前确保 data 目录存在 ---
# 将目录检查和创建逻辑移到这里,在首次需要写入前执行
try:
# exist_ok=True 意味着如果目录已存在也不会报错
os.makedirs(data_dir, exist_ok=True)
except OSError as e:
# 处理可能的权限错误等
logger.error(f"无法创建数据目录 {data_dir}: {e}")
# 根据你的错误处理逻辑,可能需要在这里返回错误或抛出异常
# 暂且返回 None 或抛出,避免继续执行导致问题
raise RuntimeError(f"无法创建必要的数据目录 {data_dir}") from e
# --- 目录检查结束 ---
class TelemetryHeartBeatTask(AsyncTask):
HEARTBEAT_INTERVAL = 300
# 检查是否已经有保存的UUID
if os.path.exists(UUID_FILE):
try:
with open(UUID_FILE, "r", encoding="utf-8") as f: # 指定 encoding
data = json.load(f)
if "client_id" in data:
logger.debug(f"从本地文件读取客户端ID: {UUID_FILE}")
return data["client_id"]
except (json.JSONDecodeError, IOError) as e:
logger.warning(f"读取UUID文件 {UUID_FILE} 出错: {e}将生成新的UUID")
except Exception as e: # 捕捉其他可能的异常
logger.error(f"读取UUID文件 {UUID_FILE} 时发生未知错误: {e}")
def __init__(self):
super().__init__(task_name="Telemetry Heart Beat Task", run_interval=self.HEARTBEAT_INTERVAL)
self.server_url = TELEMETRY_SERVER_URL
"""遥测服务地址"""
# 如果没有保存的UUID或读取出错则生成新的
client_id = generate_unique_id()
logger.info(f"生成新的客户端ID: {client_id}")
self.client_uuid = local_storage["mmc_uuid"] if "mmc_uuid" in local_storage else None
"""客户端UUID"""
# 保存UUID到文件
try:
# 再次确认目录存在 (虽然理论上前面已创建,但更保险)
os.makedirs(data_dir, exist_ok=True)
with open(UUID_FILE, "w", encoding="utf-8") as f: # 指定 encoding
json.dump({"client_id": client_id}, f, indent=4) # 添加 indent 使json可读
logger.info(f"已保存新生成的客户端ID到本地文件: {UUID_FILE}")
except IOError as e:
logger.error(f"保存UUID时出错: {UUID_FILE} - {e}")
except Exception as e: # 捕捉其他可能的异常
logger.error(f"保存UUID文件 {UUID_FILE} 时发生未知错误: {e}")
self.info_dict = self._get_sys_info()
"""系统信息字典"""
return client_id
@staticmethod
def _get_sys_info() -> dict[str, str]:
"""获取系统信息"""
info_dict = {
"os_type": "Unknown",
"py_version": platform.python_version(),
"mmc_version": global_config.MAI_VERSION,
}
match platform.system():
case "Windows":
info_dict["os_type"] = "Windows"
case "Linux":
info_dict["os_type"] = "Linux"
case "Darwin":
info_dict["os_type"] = "macOS"
case _:
info_dict["os_type"] = "Unknown"
# 生成客户端唯一ID
def generate_unique_id():
# 基于机器码生成唯一ID同一台机器上生成的UUID是固定的只要机器码不变
import hashlib
return info_dict
system_info = platform.system()
machine_code = None
async def _req_uuid(self) -> bool:
"""
向服务端请求UUID不应在已存在UUID的情况下调用会覆盖原有的UUID
"""
try:
if system_info == "Windows":
# 使用wmic命令获取主机UUID更稳定
result = subprocess.check_output(
"wmic csproduct get uuid", shell=True, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL
)
lines = result.decode(errors="ignore").splitlines()
# 过滤掉空行和表头只取有效UUID
uuids = [line.strip() for line in lines if line.strip() and line.strip().lower() != "uuid"]
if uuids:
uuid_val = uuids[0]
# logger.debug(f"主机UUID: {uuid_val}")
# 增加无效值判断
if uuid_val and uuid_val.lower() not in ["to be filled by o.e.m.", "none", "", "standard"]:
machine_code = uuid_val
elif system_info == "Linux":
# 优先读取 /etc/machine-id其次 /var/lib/dbus/machine-id取第一个非空且内容有效的
for path in ["/etc/machine-id", "/var/lib/dbus/machine-id"]:
if os.path.exists(path):
with open(path, "r") as f:
code = f.read().strip()
# 只要内容非空且不是全0
if code and set(code) != {"0"}:
machine_code = code
break
elif system_info == "Darwin":
# macOS: 使用IOPlatformUUID
result = subprocess.check_output(
"ioreg -rd1 -c IOPlatformExpertDevice | awk '/IOPlatformUUID/'", shell=True
)
uuid_line = result.decode(errors="ignore")
# 解析出 "IOPlatformUUID" = "xxxx-xxxx-xxxx-xxxx"
import re
m = re.search(r'"IOPlatformUUID"\s*=\s*"([^"]+)"', uuid_line)
if m:
uuid_val = m.group(1)
logger.debug(f"IOPlatformUUID: {uuid_val}")
if uuid_val and uuid_val.lower() not in ["to be filled by o.e.m.", "none", "", "standard"]:
machine_code = uuid_val
except Exception as e:
logger.debug(f"获取机器码失败: {e}")
# 如果主板序列号无效尝试用MAC地址
if not machine_code:
try:
mac = uuid.getnode()
if (mac >> 40) % 2 == 0: # 不是本地伪造MAC
machine_code = str(mac)
except Exception as e:
logger.debug(f"获取MAC地址失败: {e}")
def md5_to_uuid(md5hex):
# 将32位md5字符串格式化为8-4-4-4-12的UUID格式
return f"{md5hex[0:8]}-{md5hex[8:12]}-{md5hex[12:16]}-{md5hex[16:20]}-{md5hex[20:32]}"
if machine_code:
# print(f"machine_code={machine_code!r}") # 可用于调试
md5 = hashlib.md5(machine_code.encode("utf-8")).hexdigest()
uuid_str = md5_to_uuid(md5)
else:
uuid_str = str(uuid.uuid4())
unique_id = f"{system_info}-{uuid_str}"
return unique_id
def send_heartbeat(server_url, client_id):
"""向服务器发送心跳"""
sys = platform.system()
try:
headers = {"Client-ID": client_id, "User-Agent": f"HeartbeatClient/{client_id[:8]}"}
data = json.dumps(
{"system": sys, "Version": global_config.MAI_VERSION},
)
logger.debug(f"正在发送心跳到服务器: {server_url}")
logger.debug(f"心跳数据: {data}")
response = requests.post(f"{server_url}/api/clients", headers=headers, data=data)
if response.status_code == 201:
data = response.json()
logger.debug(f"心跳发送成功。服务器响应: {data}")
return True
else:
logger.debug(f"心跳发送失败。状态码: {response.status_code}, 响应内容: {response.text}")
if "deploy_time" not in local_storage:
logger.error("本地存储中缺少部署时间无法请求UUID")
return False
except requests.RequestException as e:
# 如果请求异常,可能是网络问题,不记录错误
logger.debug(f"发送心跳时出错: {e}")
return False
try_count: int = 0
while True:
# 如果不存在则向服务端请求一个新的UUID注册客户端
logger.info("正在向遥测服务端请求UUID...")
try:
response = requests.post(
f"{TELEMETRY_SERVER_URL}/stat/reg_client",
json={"deploy_time": local_storage["deploy_time"]},
)
class HeartbeatThread(threading.Thread):
"""心跳线程类"""
if response.status_code == 200:
data = response.json()
client_id = data.get("mmc_uuid")
if client_id:
# 将UUID存储到本地
local_storage["mmc_uuid"] = client_id
self.client_uuid = client_id
logger.info(f"成功获取UUID: {self.client_uuid}")
return True # 成功获取UUID返回True
else:
logger.error("无效的服务端响应")
else:
logger.error(f"请求UUID失败状态码: {response.status_code}, 响应内容: {response.text}")
except requests.RequestException as e:
logger.error(f"请求UUID时出错: {e}") # 可能是网络问题
def __init__(self, server_url, interval):
super().__init__(daemon=True) # 设置为守护线程,主程序结束时自动结束
self.server_url = server_url
self.interval = interval
self.client_id = get_unique_id()
self.running = True
self.stop_event = threading.Event() # 添加事件对象用于可中断的等待
self.last_heartbeat_time = 0 # 记录上次发送心跳的时间
def run(self):
"""线程运行函数"""
logger.debug(f"心跳线程已启动客户端ID: {self.client_id}")
while self.running:
# 发送心跳
if send_heartbeat(self.server_url, self.client_id):
logger.info(f"{self.interval}秒后发送下一次心跳...")
# 请求失败,重试次数+1
try_count += 1
if try_count > 3:
# 如果超过3次仍然失败则退出
logger.error("获取UUID失败请检查网络连接或服务端状态")
return False
else:
logger.info(f"{self.interval}秒后重试...")
# 如果可以重试,等待后继续(指数退避)
await asyncio.sleep(4**try_count)
self.last_heartbeat_time = time.time()
async def _send_heartbeat(self):
"""向服务器发送心跳"""
try:
headers = {
"Client-UUID": self.client_uuid,
"User-Agent": f"HeartbeatClient/{self.client_uuid[:8]}",
}
# 使用可中断的等待代替 sleep
# 每秒检查一次是否应该停止或发送心跳
remaining_wait = self.interval
while remaining_wait > 0 and self.running:
# 每次最多等待1秒便于及时响应停止请求
wait_time = min(1, remaining_wait)
if self.stop_event.wait(wait_time):
break # 如果事件被设置,立即退出等待
remaining_wait -= wait_time
logger.debug(f"正在发送心跳到服务器: {self.server_url}")
# 检查是否由于外部原因导致间隔异常延长
if time.time() - self.last_heartbeat_time >= self.interval * 1.5:
logger.warning("检测到心跳间隔异常延长,立即发送心跳")
break
response = requests.post(
f"{self.server_url}/stat/client_heartbeat",
headers=headers,
json=self.info_dict,
)
def stop(self):
"""停止线程"""
self.running = False
self.stop_event.set() # 设置事件,中断等待
logger.debug("心跳线程已收到停止信号")
# 处理响应
if 200 <= response.status_code < 300:
# 成功
logger.debug(f"心跳发送成功,状态码: {response.status_code}")
elif response.status_code == 403:
# 403 Forbidden
logger.error(
"心跳发送失败403 Forbidden: 可能是UUID无效或未注册。"
"处理措施重置UUID下次发送心跳时将尝试重新注册。"
)
self.client_uuid = None
del local_storage["mmc_uuid"] # 删除本地存储的UUID
else:
# 其他错误
logger.error(f"心跳发送失败,状态码: {response.status_code}, 响应内容: {response.text}")
except requests.RequestException as e:
logger.error(f"心跳发送失败: {e}")
def main():
if global_config.remote_enable:
"""主函数,启动心跳线程"""
# 配置
server_url = "http://hyybuth.xyz:10058"
# server_url = "http://localhost:10058"
heartbeat_interval = 300 # 5分钟
async def run(self):
# 发送心跳
if global_config.remote_enable:
if self.client_uuid is None:
if not await self._req_uuid():
logger.error("获取UUID失败跳过此次心跳")
return
# 创建并启动心跳线程
heartbeat_thread = HeartbeatThread(server_url, heartbeat_interval)
heartbeat_thread.start()
return heartbeat_thread # 返回线程对象,便于外部控制
return None
# --- 测试用例 ---
if __name__ == "__main__":
print("测试唯一ID生成")
print("唯一ID:", get_unique_id())
await self._send_heartbeat()

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
[inner]
version = "1.6.2.3"
version = "1.6.2.4"
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
#如果你想要修改配置文件请在修改后将version的值进行变更
@ -130,8 +130,9 @@ enable_nickname_mapping = false # 绰号映射功能总开关(默认关闭,
max_nicknames_in_prompt = 10 # Prompt 中最多注入的绰号数量防止token数量爆炸
nickname_probability_smoothing = 1 # 绰号加权随机选择的平滑因子
nickname_queue_max_size = 100 # 绰号处理队列最大容量
nickname_process_sleep_interval = 5 # 绰号处理进程休眠间隔(秒)
nickname_process_sleep_interval = 5 # 绰号处理进程休眠间隔(秒)不建议超过5否则大概率导致结束过程中超时
nickname_analysis_history_limit = 30 # 绰号处理可见最大上下文
nickname_analysis_probability = 0.1 # 绰号随机概率命中,该值越大,绰号分析越频繁
[memory]
build_memory_interval = 2000 # 记忆构建间隔 单位秒 间隔越低,麦麦学习越多,但是冗余信息也会增多
@ -149,7 +150,7 @@ consolidation_similarity_threshold = 0.7 # 相似度阈值
consolidation_check_percentage = 0.01 # 检查节点比例
#不希望记忆的词,已经记忆的不会受到影响
memory_ban_words = [
memory_ban_words = [
# "403","张三"
]
@ -194,19 +195,43 @@ enable_kaomoji_protection = false # 是否启用颜文字保护
model_max_output_length = 256 # 模型单次返回的最大token数
[remote] #发送统计信息,主要是看全球有多少只麦麦
enable = true
enable = false
[experimental] #实验性功能
enable_friend_chat = false # 是否启用好友聊天
enable_friend_chat = true # 是否启用好友聊天
enable_friend_whitelist = true # 是否启用好友聊天白名单
talk_allowed_private = [] # 可以回复消息的QQ号
pfc_chatting = false # 是否启用PFC聊天该功能仅作用于私聊与回复模式独立
api_polling_max_retries = 3
enable_pfc_reply_checker = true # 是否启用 PFC 的回复检查器
pfc_message_buffer_size = 2 # PFC 聊天消息缓冲数量,有利于使聊天节奏更加紧凑流畅,请根据实际 LLM 响应速度进行调整默认2条
api_polling_max_retries = 3 # 神秘小功能
rename_person = true # 是否启用改名工具,可以让麦麦对唯一名进行更改,可能可以更拟人地称呼他人,但是也可能导致记忆混淆的问题
[idle_chat]
enable_idle_chat = false # 是否启用 pfc 主动发言
[pfc]
enable_pfc_chatting = true # 是否启用PFC聊天该功能仅作用于私聊与回复模式独立
pfc_message_buffer_size = 2 # PFC 聊天消息缓冲数量,有利于使聊天节奏更加紧凑流畅,请根据实际 LLM 响应速度进行调整默认2条
pfc_recent_history_display_count = 20 # PFC 对话最大可见上下文
[[pfc.checker]]
enable_pfc_reply_checker = true # 是否启用 PFC 的回复检查器
pfc_max_reply_attempts = 3 # 发言最多尝试次数
pfc_max_chat_history_for_checker = 50 # checker聊天记录最大可见上文长度
[[pfc.emotion]]
pfc_emotion_update_intensity = 0.6 # 情绪更新强度
pfc_emotion_history_count = 5 # 情绪更新最大可见上下文长度
[[pfc.relationship]]
pfc_relationship_incremental_interval = 10 # 关系值增值强度
pfc_relationship_incremental_msg_count = 10 # 会话中,关系值判断最大可见上下文
pfc_relationship_incremental_default_change = 1.0 # 会话中,关系值默认更新值(当 llm 返回错误时默认采用该值)
pfc_relationship_incremental_max_change = 5.0 # 会话中,关系值最大可变值
pfc_relationship_final_msg_count = 30 # 会话结束时,关系值判断最大可见上下文
pfc_relationship_final_default_change =5.0 # 会话结束时,关系值默认更新值
pfc_relationship_final_max_change = 50.0 # 会话结束时,关系值最大可变值
[[pfc.fallback]]
pfc_historical_fallback_exclude_seconds = 7200 # pfc 翻看聊天记录排除最近时长
[[pfc.idle_chat]]
enable_idle_chat = true # 是否启用 pfc 主动发言
idle_check_interval = 10 # 检查间隔10分钟检查一次
min_cooldown = 7200 # 最短冷却时间2小时 (7200秒)
max_cooldown = 18000 # 最长冷却时间5小时 (18000秒)
@ -306,14 +331,6 @@ temp = 0.3
pri_in = 2
pri_out = 8
# PFC 关系评估LLM
[model.llm_PFC_relationship_eval]
name = "Pro/deepseek-ai/DeepSeek-V3" # 或者其他你认为适合判断任务的模型
provider = "SILICONFLOW"
temp = 0.4
pri_in = 2
pri_out = 8
#绰号映射生成模型
[model.llm_nickname_mapping]
name = "Qwen/Qwen2.5-32B-Instruct"
@ -338,6 +355,16 @@ temp = 0.3
pri_in = 2
pri_out = 8
# PFC 关系评估LLM
[model.llm_PFC_relationship_eval]
name = "deepseek-ai/DeepSeek-V3"
provider = "SILICONFLOW"
temp = 0.4
max_tokens = 512
pri_in = 2
pri_out = 8
#以下模型暂时没有使用!!
#以下模型暂时没有使用!!
#以下模型暂时没有使用!!