This commit is contained in:
tcmofashi
2025-05-01 02:20:49 +08:00
47 changed files with 1432 additions and 1006 deletions

View File

@@ -61,7 +61,7 @@
### 📢 版本信息 ### 📢 版本信息
**最新版本: v0.6.2** ([查看更新日志](changelogs/changelog.md)) **最新版本: v0.6.3** ([查看更新日志](changelogs/changelog.md))
> [!WARNING] > [!WARNING]
> 请阅读教程后更新!!!!!!! > 请阅读教程后更新!!!!!!!
> 请阅读教程后更新!!!!!!! > 请阅读教程后更新!!!!!!!
@@ -110,19 +110,20 @@
- [🚀 最新版本部署教程](https://docs.mai-mai.org/manual/deployment/mmc_deploy_windows.html) - 基于MaiCore的新版本部署方式与旧版本不兼容 - [🚀 最新版本部署教程](https://docs.mai-mai.org/manual/deployment/mmc_deploy_windows.html) - 基于MaiCore的新版本部署方式与旧版本不兼容
## 🎯 功能介绍 ## 🎯 0.6.3 功能介绍
| 模块 | 主要功能 | 特点 | | 模块 | 主要功能 | 特点 |
|----------|------------------------------------------------------------------|-------| |----------|------------------------------------------------------------------|-------|
| 💬 聊天系统 | • 心流/推理聊天<br>• 关键词主动发言<br>• 多模型支持<br>• 动态prompt构建<br>• 私聊功能(PFC) | 拟人化交互 | | 💬 聊天系统 | • **统一调控不同回复逻辑**<br>• 智能交互模式 (普通聊天/专注聊天)<br>• 关键词主动发言<br>• 多模型支持<br>• 动态prompt构建<br>• 私聊功能(PFC)增强 | 拟人化交互 |
| 🧠 心流系统 | • 实时思考生成<br>• 自动启停机制<br>• 日程系统联动<br>工具调用能力 | 智能化决策 | | 🧠 心流系统 | • 实时思考生成<br> **智能状态管理**<br>**概率回复机制**<br> 自动启停机制<br>• 日程系统联动<br>**上下文感知工具调用** | 智能化决策 |
| 🧠 记忆系统 | • 优化记忆抽取<br>• 海马体记忆机制<br>• 聊天记录概括 | 持久化记忆 | | 🧠 记忆系统 | • **记忆整合与提取**<br>• 海马体记忆机制<br>• 聊天记录概括 | 持久化记忆 |
| 😊 表情系统 | • 情绪匹配发送<br>• GIF支持<br>• 自动收集与审查 | 丰富表达 | | 😊 表情系统 | • **全新表情包系统**<br>**优化选择逻辑**<br> 情绪匹配发送<br>• GIF支持<br>• 自动收集与审查 | 丰富表达 |
| 📅 日程系统 | • 动态日程生成<br>• 自定义想象力<br>• 思维流联动 | 智能规划 | | 📅 日程系统 | • 动态日程生成<br>• 自定义想象力<br>• 思维流联动 | 智能规划 |
| 👥 关系系统 | • 关系管理优化<br>• 丰富接口支持<br>• 个性化交互 | 深度社交 | | 👥 关系系统 | • **工具调用动态更新**<br> 关系管理优化<br>• 丰富接口支持<br>• 个性化交互 | 深度社交 |
| 📊 统计系统 | • 使用数据统计<br>• LLM调用记录<br>• 实时控制台显示 | 数据可视 | | 📊 统计系统 | • 使用数据统计<br>• LLM调用记录<br>• 实时控制台显示 | 数据可视 |
| 🔧 系统功能 | • 优雅关闭机制<br>• 自动数据保存<br>异常处理完善 | 稳定可靠 | | 🛠️ 工具系统 | • **LPMM知识库集成**<br>**上下文感知调用**<br>• 知识获取工具<br>• 自动注册机制<br>多工具支持 | 扩展功能 |
| 🛠️ 工具系统 | • 知识获取工具<br>• 自动注册机制<br>• 多工具支持 | 扩展功能 | | 📚 **知识库(LPMM)** | • **全新LPMM系统**<br>**强大的信息检索能力** | 知识增强 |
| ✨ **昵称系统** | • **自动为群友取昵称**<br>**降低认错人概率** (早期阶段) | 身份识别 |
## 📐 项目架构 ## 📐 项目架构
@@ -142,18 +143,6 @@ graph TD
E --> M[情绪识别] E --> M[情绪识别]
``` ```
## 开发计划TODOLIST
- 人格功能WIP
- 对特定对象的侧写功能
- 图片发送转发功能WIP
- 幽默和meme功能WIP
- 兼容gif的解析和保存
- 小程序转发链接解析
- 修复已知bug
- 自动生成的回复逻辑,例如自生成的回复方向,回复风格
## ✍如何给本项目报告BUG/提交建议/做贡献 ## ✍如何给本项目报告BUG/提交建议/做贡献
MaiCore是一个开源项目我们非常欢迎你的参与。你的贡献无论是提交bug报告、功能需求还是代码pr都对项目非常宝贵。我们非常感谢你的支持🎉 但无序的讨论会降低沟通效率,进而影响问题的解决速度,因此在提交任何贡献前,请务必先阅读本项目的[贡献指南](depends-data/CONTRIBUTE.md)(待补完) MaiCore是一个开源项目我们非常欢迎你的参与。你的贡献无论是提交bug报告、功能需求还是代码pr都对项目非常宝贵。我们非常感谢你的支持🎉 但无序的讨论会降低沟通效率,进而影响问题的解决速度,因此在提交任何贡献前,请务必先阅读本项目的[贡献指南](depends-data/CONTRIBUTE.md)(待补完)

View File

@@ -33,7 +33,7 @@
- 调整了部分配置项的默认值 - 调整了部分配置项的默认值
- 调整了配置项的顺序,将 `groups` 配置项移到了更靠前的位置 - 调整了配置项的顺序,将 `groups` 配置项移到了更靠前的位置
-`message` 配置项中: -`message` 配置项中:
- 新增了 `max_response_length` 参数 - 新增了 `model_max_output_length` 参数
-`willing` 配置项中新增了 `emoji_response_penalty` 参数 -`willing` 配置项中新增了 `emoji_response_penalty` 参数
-`personality` 配置项中的 `prompt_schedule` 重命名为 `prompt_schedule_gen` -`personality` 配置项中的 `prompt_schedule` 重命名为 `prompt_schedule_gen`

View File

@@ -344,9 +344,6 @@ class InterestMonitorApp:
self.stream_last_active[stream_id] = subflow_entry.get( self.stream_last_active[stream_id] = subflow_entry.get(
"chat_state_changed_time" "chat_state_changed_time"
) # 存储原始时间戳 ) # 存储原始时间戳
self.stream_last_interaction[stream_id] = subflow_entry.get(
"last_interaction_time"
) # 存储原始时间戳
# 添加数据点 (使用顶层时间戳) # 添加数据点 (使用顶层时间戳)
new_stream_history[stream_id].append((entry_timestamp, interest_level_float)) new_stream_history[stream_id].append((entry_timestamp, interest_level_float))

View File

@@ -47,7 +47,7 @@ class BotConfig:
MAX_CONTEXT_SIZE: int # 上下文最大消息数 MAX_CONTEXT_SIZE: int # 上下文最大消息数
emoji_chance: float # 发送表情包的基础概率 emoji_chance: float # 发送表情包的基础概率
thinking_timeout: int # 思考时间 thinking_timeout: int # 思考时间
max_response_length: int # 最大回复长度 model_max_output_length: int # 最大回复长度
message_buffer: bool # 消息缓冲器 message_buffer: bool # 消息缓冲器
ban_words: set ban_words: set
@@ -132,7 +132,7 @@ class BotConfig:
# llm_reasoning_minor: Dict[str, str] # llm_reasoning_minor: Dict[str, str]
llm_normal: Dict[str, str] # LLM普通 llm_normal: Dict[str, str] # LLM普通
llm_topic_judge: Dict[str, str] # LLM话题判断 llm_topic_judge: Dict[str, str] # LLM话题判断
llm_summary_by_topic: Dict[str, str] # LLM话题总结 llm_summary: Dict[str, str] # LLM话题总结
llm_emotion_judge: Dict[str, str] # LLM情感判断 llm_emotion_judge: Dict[str, str] # LLM情感判断
embedding: Dict[str, str] # 嵌入 embedding: Dict[str, str] # 嵌入
vlm: Dict[str, str] # VLM vlm: Dict[str, str] # VLM

View File

@@ -621,25 +621,24 @@ CHAT_IMAGE_STYLE_CONFIG = {
}, },
} }
# 兴趣log # HFC log
INTEREST_STYLE_CONFIG = { HFC_STYLE_CONFIG = {
"advanced": { "advanced": {
"console_format": ( "console_format": (
"<white>{time:YYYY-MM-DD HH:mm:ss}</white> | " "<white>{time:YYYY-MM-DD HH:mm:ss}</white> | "
"<level>{level: <8}</level> | " "<level>{level: <8}</level> | "
"<light-yellow>兴趣</light-yellow> | " "<light-green>专注聊天</light-green> | "
"<level>{message}</level>" "<level>{message}</level>"
), ),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 兴趣 | {message}", "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注聊天 | {message}",
}, },
"simple": { "simple": {
"console_format": ( "console_format": ("<level>{time:MM-DD HH:mm}</level> | <light-green>专注聊天 | {message}</light-green>"),
"<level>{time:MM-DD HH:mm}</level> | <light-green>兴趣</light-green> | <light-green>{message}</light-green>" "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注聊天 | {message}",
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 兴趣 | {message}",
}, },
} }
CONFIRM_STYLE_CONFIG = { CONFIRM_STYLE_CONFIG = {
"console_format": "<RED>{message}</RED>", # noqa: E501 "console_format": "<RED>{message}</RED>", # noqa: E501
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | EULA与PRIVACY确认 | {message}", "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | EULA与PRIVACY确认 | {message}",
@@ -847,7 +846,7 @@ CONFIG_STYLE_CONFIG = CONFIG_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else CONFIG
TOOL_USE_STYLE_CONFIG = TOOL_USE_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else TOOL_USE_STYLE_CONFIG["advanced"] TOOL_USE_STYLE_CONFIG = TOOL_USE_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else TOOL_USE_STYLE_CONFIG["advanced"]
PFC_STYLE_CONFIG = PFC_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else PFC_STYLE_CONFIG["advanced"] PFC_STYLE_CONFIG = PFC_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else PFC_STYLE_CONFIG["advanced"]
LPMM_STYLE_CONFIG = LPMM_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else LPMM_STYLE_CONFIG["advanced"] LPMM_STYLE_CONFIG = LPMM_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else LPMM_STYLE_CONFIG["advanced"]
INTEREST_STYLE_CONFIG = INTEREST_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else INTEREST_STYLE_CONFIG["advanced"] HFC_STYLE_CONFIG = HFC_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else HFC_STYLE_CONFIG["advanced"]
TIANYI_STYLE_CONFIG = TIANYI_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else TIANYI_STYLE_CONFIG["advanced"] TIANYI_STYLE_CONFIG = TIANYI_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else TIANYI_STYLE_CONFIG["advanced"]
MODEL_UTILS_STYLE_CONFIG = MODEL_UTILS_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else MODEL_UTILS_STYLE_CONFIG["advanced"] MODEL_UTILS_STYLE_CONFIG = MODEL_UTILS_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else MODEL_UTILS_STYLE_CONFIG["advanced"]
PROMPT_STYLE_CONFIG = PROMPT_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else PROMPT_STYLE_CONFIG["advanced"] PROMPT_STYLE_CONFIG = PROMPT_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else PROMPT_STYLE_CONFIG["advanced"]

View File

@@ -23,7 +23,7 @@ from src.common.logger import (
PFC_ACTION_PLANNER_STYLE_CONFIG, PFC_ACTION_PLANNER_STYLE_CONFIG,
MAI_STATE_CONFIG, MAI_STATE_CONFIG,
LPMM_STYLE_CONFIG, LPMM_STYLE_CONFIG,
INTEREST_STYLE_CONFIG, HFC_STYLE_CONFIG,
TIANYI_STYLE_CONFIG, TIANYI_STYLE_CONFIG,
REMOTE_STYLE_CONFIG, REMOTE_STYLE_CONFIG,
TOPIC_STYLE_CONFIG, TOPIC_STYLE_CONFIG,
@@ -68,7 +68,7 @@ MODULE_LOGGER_CONFIGS = {
"pfc_action_planner": PFC_ACTION_PLANNER_STYLE_CONFIG, # PFC私聊规划 "pfc_action_planner": PFC_ACTION_PLANNER_STYLE_CONFIG, # PFC私聊规划
"mai_state": MAI_STATE_CONFIG, # 麦麦状态 "mai_state": MAI_STATE_CONFIG, # 麦麦状态
"lpmm": LPMM_STYLE_CONFIG, # LPMM "lpmm": LPMM_STYLE_CONFIG, # LPMM
"interest": INTEREST_STYLE_CONFIG, # 兴趣 "hfc": HFC_STYLE_CONFIG, # HFC
"tianyi": TIANYI_STYLE_CONFIG, # 天依 "tianyi": TIANYI_STYLE_CONFIG, # 天依
"remote": REMOTE_STYLE_CONFIG, # 远程 "remote": REMOTE_STYLE_CONFIG, # 远程
"topic": TOPIC_STYLE_CONFIG, # 话题 "topic": TOPIC_STYLE_CONFIG, # 话题

View File

@@ -20,9 +20,9 @@ from src.common.logger_manager import get_logger
logger = get_logger("config") logger = get_logger("config")
# 考虑到实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码 # 考虑到实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码
is_test = True is_test = False
mai_version_main = "0.6.3" mai_version_main = "0.6.3"
mai_version_fix = "snapshot-5" mai_version_fix = "fix-1"
if mai_version_fix: if mai_version_fix:
if is_test: if is_test:
@@ -170,32 +170,34 @@ class BotConfig:
SCHEDULE_TEMPERATURE: float = 0.5 # 日程表温度建议0.5-1.0 SCHEDULE_TEMPERATURE: float = 0.5 # 日程表温度建议0.5-1.0
TIME_ZONE: str = "Asia/Shanghai" # 时区 TIME_ZONE: str = "Asia/Shanghai" # 时区
# message # chat
MAX_CONTEXT_SIZE: int = 15 # 上下文最大消息数 allow_focus_mode: bool = True # 是否允许专注聊天状态
emoji_chance: float = 0.2 # 发送表情包的基础概率
thinking_timeout: int = 120 # 思考时间 base_normal_chat_num: int = 3 # 最多允许多少个群进行普通聊天
max_response_length: int = 1024 # 最大回复长度 base_focused_chat_num: int = 2 # 最多允许多少个群进行专注聊天
observation_context_size: int = 12 # 心流观察到的最长上下文大小,超过这个值的上下文会被压缩
message_buffer: bool = True # 消息缓冲器 message_buffer: bool = True # 消息缓冲器
ban_words = set() ban_words = set()
ban_msgs_regex = set() ban_msgs_regex = set()
# [heartflow] # 启用启用heart_flowC(心流聊天)模式时生效, 需要填写token消耗量巨大的相关模型 # focus_chat
# 启用后麦麦会自主选择进入heart_flowC模式(持续一段时间), 进行长时间高质量的聊天
reply_trigger_threshold: float = 3.0 # 心流聊天触发阈值,越低越容易触发 reply_trigger_threshold: float = 3.0 # 心流聊天触发阈值,越低越容易触发
probability_decay_factor_per_second: float = 0.2 # 概率衰减因子,越大衰减越快
default_decay_rate_per_second: float = 0.98 # 默认衰减率,越大衰减越慢 default_decay_rate_per_second: float = 0.98 # 默认衰减率,越大衰减越慢
allow_focus_mode: bool = True # 是否允许子心流进入 FOCUSED 状态 consecutive_no_reply_threshold = 3
# sub_heart_flow_update_interval: int = 60 # 子心流更新频率,间隔 单位秒
# sub_heart_flow_freeze_time: int = 120 # 子心流冻结时间,超过这个时间没有回复,子心流会冻结,间隔 单位秒
sub_heart_flow_stop_time: int = 600 # 子心流停止时间,超过这个时间没有回复,子心流会停止,间隔 单位秒
# heart_flow_update_interval: int = 300 # 心流更新频率,间隔 单位秒
observation_context_size: int = 20 # 心流观察到的最长上下文大小,超过这个值的上下文会被压缩
compressed_length: int = 5 # 不能大于observation_context_size,心流上下文压缩的最短压缩长度超过心流观察到的上下文长度会压缩最短压缩长度为5 compressed_length: int = 5 # 不能大于observation_context_size,心流上下文压缩的最短压缩长度超过心流观察到的上下文长度会压缩最短压缩长度为5
compress_length_limit: int = 5 # 最多压缩份数,超过该数值的压缩上下文会被删除 compress_length_limit: int = 5 # 最多压缩份数,超过该数值的压缩上下文会被删除
# willing # normal_chat
model_reasoning_probability: float = 0.7 # 麦麦回答时选择推理模型(主要)模型概率
model_normal_probability: float = 0.3 # 麦麦回答时选择一般模型(次要)模型概率
emoji_chance: float = 0.2 # 发送表情包的基础概率
thinking_timeout: int = 120 # 思考时间
willing_mode: str = "classical" # 意愿模式 willing_mode: str = "classical" # 意愿模式
response_willing_amplifier: float = 1.0 # 回复意愿放大系数 response_willing_amplifier: float = 1.0 # 回复意愿放大系数
response_interested_rate_amplifier: float = 1.0 # 回复兴趣度放大系数 response_interested_rate_amplifier: float = 1.0 # 回复兴趣度放大系数
@@ -204,12 +206,6 @@ class BotConfig:
mentioned_bot_inevitable_reply: bool = False # 提及 bot 必然回复 mentioned_bot_inevitable_reply: bool = False # 提及 bot 必然回复
at_bot_inevitable_reply: bool = False # @bot 必然回复 at_bot_inevitable_reply: bool = False # @bot 必然回复
# response
response_mode: str = "heart_flow" # 回复策略
model_reasoning_probability: float = 0.7 # 麦麦回答时选择推理模型(主要)模型概率
model_normal_probability: float = 0.3 # 麦麦回答时选择一般模型(次要)模型概率
# MODEL_R1_DISTILL_PROBABILITY: float = 0.1 # R1蒸馏模型概率
# emoji # emoji
max_emoji_num: int = 200 # 表情包最大数量 max_emoji_num: int = 200 # 表情包最大数量
max_reach_deletion: bool = True # 开启则在达到最大数量时删除表情包,关闭则不会继续收集表情包 max_reach_deletion: bool = True # 开启则在达到最大数量时删除表情包,关闭则不会继续收集表情包
@@ -264,6 +260,8 @@ class BotConfig:
response_max_length = 100 # 回复允许的最大长度 response_max_length = 100 # 回复允许的最大长度
response_max_sentence_num = 3 # 回复允许的最大句子数 response_max_sentence_num = 3 # 回复允许的最大句子数
model_max_output_length: int = 800 # 最大回复长度
# remote # remote
remote_enable: bool = True # 是否启用远程控制 remote_enable: bool = True # 是否启用远程控制
@@ -277,8 +275,7 @@ class BotConfig:
# llm_reasoning_minor: Dict[str, str] = field(default_factory=lambda: {}) # llm_reasoning_minor: Dict[str, str] = field(default_factory=lambda: {})
llm_normal: Dict[str, str] = field(default_factory=lambda: {}) llm_normal: Dict[str, str] = field(default_factory=lambda: {})
llm_topic_judge: Dict[str, str] = field(default_factory=lambda: {}) llm_topic_judge: Dict[str, str] = field(default_factory=lambda: {})
llm_summary_by_topic: Dict[str, str] = field(default_factory=lambda: {}) llm_summary: Dict[str, str] = field(default_factory=lambda: {})
llm_emotion_judge: Dict[str, str] = field(default_factory=lambda: {})
embedding: Dict[str, str] = field(default_factory=lambda: {}) embedding: Dict[str, str] = field(default_factory=lambda: {})
vlm: Dict[str, str] = field(default_factory=lambda: {}) vlm: Dict[str, str] = field(default_factory=lambda: {})
moderation: Dict[str, str] = field(default_factory=lambda: {}) moderation: Dict[str, str] = field(default_factory=lambda: {})
@@ -409,63 +406,62 @@ class BotConfig:
config.BOT_NICKNAME = bot_config.get("nickname", config.BOT_NICKNAME) config.BOT_NICKNAME = bot_config.get("nickname", config.BOT_NICKNAME)
config.BOT_ALIAS_NAMES = bot_config.get("alias_names", config.BOT_ALIAS_NAMES) config.BOT_ALIAS_NAMES = bot_config.get("alias_names", config.BOT_ALIAS_NAMES)
def response(parent: dict): def chat(parent: dict):
response_config = parent["response"] chat_config = parent["chat"]
config.model_reasoning_probability = response_config.get( config.allow_focus_mode = chat_config.get("allow_focus_mode", config.allow_focus_mode)
config.base_normal_chat_num = chat_config.get("base_normal_chat_num", config.base_normal_chat_num)
config.base_focused_chat_num = chat_config.get("base_focused_chat_num", config.base_focused_chat_num)
config.observation_context_size = chat_config.get(
"observation_context_size", config.observation_context_size
)
config.message_buffer = chat_config.get("message_buffer", config.message_buffer)
config.ban_words = chat_config.get("ban_words", config.ban_words)
for r in chat_config.get("ban_msgs_regex", config.ban_msgs_regex):
config.ban_msgs_regex.add(re.compile(r))
def normal_chat(parent: dict):
normal_chat_config = parent["normal_chat"]
config.model_reasoning_probability = normal_chat_config.get(
"model_reasoning_probability", config.model_reasoning_probability "model_reasoning_probability", config.model_reasoning_probability
) )
config.model_normal_probability = response_config.get( config.model_normal_probability = normal_chat_config.get(
"model_normal_probability", config.model_normal_probability "model_normal_probability", config.model_normal_probability
) )
config.emoji_chance = normal_chat_config.get("emoji_chance", config.emoji_chance)
config.thinking_timeout = normal_chat_config.get("thinking_timeout", config.thinking_timeout)
def heartflow(parent: dict): config.willing_mode = normal_chat_config.get("willing_mode", config.willing_mode)
heartflow_config = parent["heartflow"] config.response_willing_amplifier = normal_chat_config.get(
config.sub_heart_flow_stop_time = heartflow_config.get( "response_willing_amplifier", config.response_willing_amplifier
"sub_heart_flow_stop_time", config.sub_heart_flow_stop_time )
config.response_interested_rate_amplifier = normal_chat_config.get(
"response_interested_rate_amplifier", config.response_interested_rate_amplifier
)
config.down_frequency_rate = normal_chat_config.get("down_frequency_rate", config.down_frequency_rate)
config.emoji_response_penalty = normal_chat_config.get(
"emoji_response_penalty", config.emoji_response_penalty
) )
if config.INNER_VERSION in SpecifierSet(">=1.3.0"):
config.observation_context_size = heartflow_config.get(
"observation_context_size", config.observation_context_size
)
config.compressed_length = heartflow_config.get("compressed_length", config.compressed_length)
config.compress_length_limit = heartflow_config.get(
"compress_length_limit", config.compress_length_limit
)
if config.INNER_VERSION in SpecifierSet(">=1.4.0"):
config.reply_trigger_threshold = heartflow_config.get(
"reply_trigger_threshold", config.reply_trigger_threshold
)
config.probability_decay_factor_per_second = heartflow_config.get(
"probability_decay_factor_per_second", config.probability_decay_factor_per_second
)
config.default_decay_rate_per_second = heartflow_config.get(
"default_decay_rate_per_second", config.default_decay_rate_per_second
)
if config.INNER_VERSION in SpecifierSet(">=1.5.1"):
config.allow_focus_mode = heartflow_config.get("allow_focus_mode", config.allow_focus_mode)
def willing(parent: dict): config.mentioned_bot_inevitable_reply = normal_chat_config.get(
willing_config = parent["willing"] "mentioned_bot_inevitable_reply", config.mentioned_bot_inevitable_reply
config.willing_mode = willing_config.get("willing_mode", config.willing_mode) )
config.at_bot_inevitable_reply = normal_chat_config.get(
"at_bot_inevitable_reply", config.at_bot_inevitable_reply
)
if config.INNER_VERSION in SpecifierSet(">=0.0.11"): def focus_chat(parent: dict):
config.response_willing_amplifier = willing_config.get( focus_chat_config = parent["focus_chat"]
"response_willing_amplifier", config.response_willing_amplifier config.compressed_length = focus_chat_config.get("compressed_length", config.compressed_length)
) config.compress_length_limit = focus_chat_config.get("compress_length_limit", config.compress_length_limit)
config.response_interested_rate_amplifier = willing_config.get( config.reply_trigger_threshold = focus_chat_config.get(
"response_interested_rate_amplifier", config.response_interested_rate_amplifier "reply_trigger_threshold", config.reply_trigger_threshold
) )
config.down_frequency_rate = willing_config.get("down_frequency_rate", config.down_frequency_rate) config.default_decay_rate_per_second = focus_chat_config.get(
config.emoji_response_penalty = willing_config.get( "default_decay_rate_per_second", config.default_decay_rate_per_second
"emoji_response_penalty", config.emoji_response_penalty )
) config.consecutive_no_reply_threshold = focus_chat_config.get(
if config.INNER_VERSION in SpecifierSet(">=1.2.5"): "consecutive_no_reply_threshold", config.consecutive_no_reply_threshold
config.mentioned_bot_inevitable_reply = willing_config.get( )
"mentioned_bot_inevitable_reply", config.mentioned_bot_inevitable_reply
)
config.at_bot_inevitable_reply = willing_config.get(
"at_bot_inevitable_reply", config.at_bot_inevitable_reply
)
def model(parent: dict): def model(parent: dict):
# 加载模型配置 # 加载模型配置
@@ -476,8 +472,7 @@ class BotConfig:
# "llm_reasoning_minor", # "llm_reasoning_minor",
"llm_normal", "llm_normal",
"llm_topic_judge", "llm_topic_judge",
"llm_summary_by_topic", "llm_summary",
"llm_emotion_judge",
"vlm", "vlm",
"embedding", "embedding",
"llm_tool_use", "llm_tool_use",
@@ -556,26 +551,6 @@ class BotConfig:
logger.error(f"模型 {item} 在config中不存在请检查或尝试更新配置文件") logger.error(f"模型 {item} 在config中不存在请检查或尝试更新配置文件")
raise KeyError(f"模型 {item} 在config中不存在请检查或尝试更新配置文件") raise KeyError(f"模型 {item} 在config中不存在请检查或尝试更新配置文件")
def message(parent: dict):
msg_config = parent["message"]
config.MAX_CONTEXT_SIZE = msg_config.get("max_context_size", config.MAX_CONTEXT_SIZE)
config.emoji_chance = msg_config.get("emoji_chance", config.emoji_chance)
config.ban_words = msg_config.get("ban_words", config.ban_words)
config.thinking_timeout = msg_config.get("thinking_timeout", config.thinking_timeout)
config.response_willing_amplifier = msg_config.get(
"response_willing_amplifier", config.response_willing_amplifier
)
config.response_interested_rate_amplifier = msg_config.get(
"response_interested_rate_amplifier", config.response_interested_rate_amplifier
)
config.down_frequency_rate = msg_config.get("down_frequency_rate", config.down_frequency_rate)
for r in msg_config.get("ban_msgs_regex", config.ban_msgs_regex):
config.ban_msgs_regex.add(re.compile(r))
if config.INNER_VERSION in SpecifierSet(">=0.0.11"):
config.max_response_length = msg_config.get("max_response_length", config.max_response_length)
if config.INNER_VERSION in SpecifierSet(">=1.1.4"):
config.message_buffer = msg_config.get("message_buffer", config.message_buffer)
def memory(parent: dict): def memory(parent: dict):
memory_config = parent["memory"] memory_config = parent["memory"]
config.build_memory_interval = memory_config.get("build_memory_interval", config.build_memory_interval) config.build_memory_interval = memory_config.get("build_memory_interval", config.build_memory_interval)
@@ -650,6 +625,10 @@ class BotConfig:
config.enable_kaomoji_protection = response_splitter_config.get( config.enable_kaomoji_protection = response_splitter_config.get(
"enable_kaomoji_protection", config.enable_kaomoji_protection "enable_kaomoji_protection", config.enable_kaomoji_protection
) )
if config.INNER_VERSION in SpecifierSet(">=1.6.0"):
config.model_max_output_length = response_splitter_config.get(
"model_max_output_length", config.model_max_output_length
)
def groups(parent: dict): def groups(parent: dict):
groups_config = parent["groups"] groups_config = parent["groups"]
@@ -695,10 +674,7 @@ class BotConfig:
"personality": {"func": personality, "support": ">=0.0.0"}, "personality": {"func": personality, "support": ">=0.0.0"},
"identity": {"func": identity, "support": ">=1.2.4"}, "identity": {"func": identity, "support": ">=1.2.4"},
"schedule": {"func": schedule, "support": ">=0.0.11", "necessary": False}, "schedule": {"func": schedule, "support": ">=0.0.11", "necessary": False},
"message": {"func": message, "support": ">=0.0.0"},
"willing": {"func": willing, "support": ">=0.0.9", "necessary": False},
"emoji": {"func": emoji, "support": ">=0.0.0"}, "emoji": {"func": emoji, "support": ">=0.0.0"},
"response": {"func": response, "support": ">=0.0.0"},
"model": {"func": model, "support": ">=0.0.0"}, "model": {"func": model, "support": ">=0.0.0"},
"memory": {"func": memory, "support": ">=0.0.0", "necessary": False}, "memory": {"func": memory, "support": ">=0.0.0", "necessary": False},
"mood": {"func": mood, "support": ">=0.0.0"}, "mood": {"func": mood, "support": ">=0.0.0"},
@@ -708,7 +684,9 @@ class BotConfig:
"platforms": {"func": platforms, "support": ">=1.0.0"}, "platforms": {"func": platforms, "support": ">=1.0.0"},
"response_splitter": {"func": response_splitter, "support": ">=0.0.11", "necessary": False}, "response_splitter": {"func": response_splitter, "support": ">=0.0.11", "necessary": False},
"experimental": {"func": experimental, "support": ">=0.0.11", "necessary": False}, "experimental": {"func": experimental, "support": ">=0.0.11", "necessary": False},
"heartflow": {"func": heartflow, "support": ">=1.0.2", "necessary": False}, "chat": {"func": chat, "support": ">=1.6.0", "necessary": False},
"normal_chat": {"func": normal_chat, "support": ">=1.6.0", "necessary": False},
"focus_chat": {"func": focus_chat, "support": ">=1.6.0", "necessary": False},
} }
# 原地修改,将 字符串版本表达式 转换成 版本对象 # 原地修改,将 字符串版本表达式 转换成 版本对象

View File

@@ -62,7 +62,7 @@ def register_tool(tool_class: Type[BaseTool]):
raise ValueError(f"工具类 {tool_class.__name__} 没有定义 name 属性") raise ValueError(f"工具类 {tool_class.__name__} 没有定义 name 属性")
TOOL_REGISTRY[tool_name] = tool_class TOOL_REGISTRY[tool_name] = tool_class
logger.info(f"已注册工具: {tool_name}") logger.info(f"已注册: {tool_name}")
def discover_tools(): def discover_tools():

View File

@@ -14,7 +14,7 @@ class SearchKnowledgeFromLPMMTool(BaseTool):
"""从LPMM知识库中搜索相关信息的工具""" """从LPMM知识库中搜索相关信息的工具"""
name = "lpmm_search_knowledge" name = "lpmm_search_knowledge"
description = "从知识库中搜索相关信息" description = "从知识库中搜索相关信息,如果你需要知识,就使用这个工具"
parameters = { parameters = {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -129,7 +129,6 @@ class ToolUser:
payload = { payload = {
"model": self.llm_model_tool.model_name, "model": self.llm_model_tool.model_name,
"messages": [{"role": "user", "content": prompt}], "messages": [{"role": "user", "content": prompt}],
"max_tokens": global_config.max_response_length,
"tools": tools, "tools": tools,
"temperature": 0.2, "temperature": 0.2,
} }

View File

@@ -1,48 +0,0 @@
# 0.6.3 版本发布前待办事项
- [0.6.3]**统一化人格配置:**
- 检查代码中是否存在硬编码的人格相关配置。
- 将所有硬编码的人格配置替换为使用 `individual` 模块进行管理。
- [0.6.3]**在 Planner 中添加回复计数信息:**
- 修改 `HeartFlowChatInstance``Plan` 阶段逻辑。
- 将当前周期的回复计数(或其他相关统计信息)作为输入提供给 Planner。
- 目的是为 Planner 提供负反馈,减少连续回复或不当回复的可能性。
- [0.6.3]**恢复/检查被停止的功能:**
- 全面审查代码,特别是对比之前的版本或设计文档。
- 识别并重新启用那些暂时被禁用但应该恢复的功能。
- 确认没有核心功能意外丢失。
- [0.6.3]**参数提取与配置化:**
- 识别代码中散落的各种可调参数例如概率阈值、时间间隔、次数限制、LLM 模型名称等)。
- 将这些参数统一提取到模块或类的顶部。
- 最终将这些参数移至外部配置文件(如 YAML 或 JSON 文件),方便用户自定义。
- **[0.6.3]提供 HFC (HeartFlowChatInstance) 开启/关闭选项:**
- 增加一个全局或针对特定子心流的配置选项。
- 允许用户控制是否启用 `FOCUSED` 状态以及关联的 `HeartFlowChatInstance`
- 如果禁用 HFC子心流可能只会在 `ABSENT``CHAT` 状态间切换。
- [0.6.3]**添加防破线机制 (针对接收消息):**
- 在消息处理流程的早期阶段 (例如 `HeartHC_processor` 或类似模块),增加对接收到的消息文本长度的检查。
- 对超过预设长度阈值的*接收*消息进行截断处理。
- 目的是防止过长的输入(可能包含"破限"提示词影响后续的兴趣计算、LLM 回复生成等环节。
- [0.6.3]**NormalChat 模式下的记忆与 Prompt 优化:**
- 重点审视 `NormalChatInstance` (闲聊/推理模式) 中记忆调用 (例如 `HippocampusManager` 的使用) 的方式。
- 评估在该模式下引入工具调用 (Tool Calling) 机制以更结构化访问记忆的必要性。
- 优化 `NormalChatInstance` 中与记忆检索、应用相关的 Prompt。
- [0.6.3]**完善简易兴趣监控 GUI:**
- 改进现有的、用于监控聊天兴趣度 (`InterestChatting`?) 的简单 GUI 界面。
- 使其能更清晰地展示关键参数和状态,作为查看日志之外的更直观的监控方式。
- 作为完整外部 UI 开发完成前的临时替代方案。
- [0.6.3]**修复/完善中期记忆 (Midterm Memory):**
- 检查当前中期记忆模块的状态。
- 修复已知问题,使其能够稳定运行。
- (优先级视开发时间而定)
对于有些群频繁激活HFC却不回复需要处理一下

View File

@@ -82,3 +82,13 @@
- 开发利用 LLM 和人格配置生成背景知识的功能。 - 开发利用 LLM 和人格配置生成背景知识的功能。
- 这些知识应符合角色的行为风格和可能的经历。 - 这些知识应符合角色的行为风格和可能的经历。
- 作为一种"冷启动"或丰富角色深度的方式。 - 作为一种"冷启动"或丰富角色深度的方式。
## 开发计划TODOLIST
- 人格功能WIP
- 对特定对象的侧写功能
- 图片发送转发功能WIP
- 幽默和meme功能WIP
- 小程序转发链接解析
- 自动生成的回复逻辑,例如自生成的回复方向,回复风格

View File

@@ -106,8 +106,8 @@ c HeartFChatting工作方式
- 负责所有 `SubHeartflow` 实例的生命周期管理,包括: - 负责所有 `SubHeartflow` 实例的生命周期管理,包括:
- 创建和获取 (`get_or_create_subheartflow`)。 - 创建和获取 (`get_or_create_subheartflow`)。
- 停止和清理 (`sleep_subheartflow`, `cleanup_inactive_subheartflows`)。 - 停止和清理 (`sleep_subheartflow`, `cleanup_inactive_subheartflows`)。
- 根据 `Heartflow` 的状态 (`self.mai_state_info`) 和限制条件,激活、停用或调整子心流的状态(例如 `enforce_subheartflow_limits`, `randomly_deactivate_subflows`, `evaluate_interest_and_promote`)。 - 根据 `Heartflow` 的状态 (`self.mai_state_info`) 和限制条件,激活、停用或调整子心流的状态(例如 `enforce_subheartflow_limits`, `randomly_deactivate_subflows`, `sbhf_absent_into_focus`)。
- **新增**: 通过调用 `evaluate_and_transition_subflows_by_llm` 方法,使用 LLM (配置与 `Heartflow` 主 LLM 相同) 评估处于 `ABSENT``CHAT` 状态的子心流,根据观察到的活动摘要和 `Heartflow` 的当前状态,判断是否应在 `ABSENT``CHAT` 之间进行转换 (同样受限于 `CHAT` 状态的数量上限)。 - **新增**: 通过调用 `sbhf_absent_into_chat` 方法,使用 LLM (配置与 `Heartflow` 主 LLM 相同) 评估处于 `ABSENT``CHAT` 状态的子心流,根据观察到的活动摘要和 `Heartflow` 的当前状态,判断是否应在 `ABSENT``CHAT` 之间进行转换 (同样受限于 `CHAT` 状态的数量上限)。
- **清理机制**: 通过后台任务 (`BackgroundTaskManager`) 定期调用 `cleanup_inactive_subheartflows` 方法,此方法会识别并**删除**那些处于 `ABSENT` 状态超过一小时 (`INACTIVE_THRESHOLD_SECONDS`) 的子心流实例。 - **清理机制**: 通过后台任务 (`BackgroundTaskManager`) 定期调用 `cleanup_inactive_subheartflows` 方法,此方法会识别并**删除**那些处于 `ABSENT` 状态超过一小时 (`INACTIVE_THRESHOLD_SECONDS`) 的子心流实例。
### 1.5. 消息处理与回复流程 (Message Processing vs. Replying Flow) ### 1.5. 消息处理与回复流程 (Message Processing vs. Replying Flow)
@@ -155,20 +155,20 @@ c HeartFChatting工作方式
- **初始状态**: 新创建的 `SubHeartflow` 默认为 `ABSENT` 状态。 - **初始状态**: 新创建的 `SubHeartflow` 默认为 `ABSENT` 状态。
- **`ABSENT` -> `CHAT` (激活闲聊)**: - **`ABSENT` -> `CHAT` (激活闲聊)**:
- **触发条件**: `Heartflow` 的主状态 (`MaiState`) 允许 `CHAT` 模式,且当前 `CHAT` 状态的子心流数量未达上限。 - **触发条件**: `Heartflow` 的主状态 (`MaiState`) 允许 `CHAT` 模式,且当前 `CHAT` 状态的子心流数量未达上限。
- **判定机制**: `SubHeartflowManager` 中的 `evaluate_and_transition_subflows_by_llm` 方法调用大模型(LLM)。LLM 读取该群聊的近期内容和结合自身个性信息,判断是否"想"在该群开始聊天。 - **判定机制**: `SubHeartflowManager` 中的 `sbhf_absent_into_chat` 方法调用大模型(LLM)。LLM 读取该群聊的近期内容和结合自身个性信息,判断是否"想"在该群开始聊天。
- **执行**: 若 LLM 判断为是,且名额未满,`SubHeartflowManager` 调用 `change_chat_state(ChatState.CHAT)` - **执行**: 若 LLM 判断为是,且名额未满,`SubHeartflowManager` 调用 `change_chat_state(ChatState.CHAT)`
- **`CHAT` -> `FOCUSED` (激活专注)**: - **`CHAT` -> `FOCUSED` (激活专注)**:
- **触发条件**: 子心流处于 `CHAT` 状态,其内部维护的"开屎热聊"概率 (`InterestChatting.start_hfc_probability`) 达到预设阈值(表示对当前聊天兴趣浓厚),同时 `Heartflow` 的主状态允许 `FOCUSED` 模式,且 `FOCUSED` 名额未满。 - **触发条件**: 子心流处于 `CHAT` 状态,其内部维护的"开屎热聊"概率 (`InterestChatting.start_hfc_probability`) 达到预设阈值(表示对当前聊天兴趣浓厚),同时 `Heartflow` 的主状态允许 `FOCUSED` 模式,且 `FOCUSED` 名额未满。
- **判定机制**: `SubHeartflowManager` 中的 `evaluate_interest_and_promote` 方法定期检查满足条件的 `CHAT` 子心流。 - **判定机制**: `SubHeartflowManager` 中的 `sbhf_absent_into_focus` 方法定期检查满足条件的 `CHAT` 子心流。
- **执行**: 若满足所有条件,`SubHeartflowManager` 调用 `change_chat_state(ChatState.FOCUSED)` - **执行**: 若满足所有条件,`SubHeartflowManager` 调用 `change_chat_state(ChatState.FOCUSED)`
- **注意**: 无法从 `ABSENT` 直接跳到 `FOCUSED`,必须先经过 `CHAT` - **注意**: 无法从 `ABSENT` 直接跳到 `FOCUSED`,必须先经过 `CHAT`
- **`FOCUSED` -> `ABSENT` (退出专注)**: - **`FOCUSED` -> `ABSENT` (退出专注)**:
- **主要途径 (内部驱动)**: 在 `FOCUSED` 状态下运行的 `HeartFlowChatInstance` 连续多次决策为 `no_reply` (例如达到 5 次,次数可配),它会通过回调函数 (`request_absent_transition`) 请求 `SubHeartflowManager` 将其状态**直接**设置为 `ABSENT` - **主要途径 (内部驱动)**: 在 `FOCUSED` 状态下运行的 `HeartFlowChatInstance` 连续多次决策为 `no_reply` (例如达到 5 次,次数可配),它会通过回调函数 (`sbhf_focus_into_absent`) 请求 `SubHeartflowManager` 将其状态**直接**设置为 `ABSENT`
- **其他途径 (外部驱动)**: - **其他途径 (外部驱动)**:
- `Heartflow` 主状态变为 `OFFLINE``SubHeartflowManager` 强制所有子心流变为 `ABSENT` - `Heartflow` 主状态变为 `OFFLINE``SubHeartflowManager` 强制所有子心流变为 `ABSENT`
- `SubHeartflowManager``FOCUSED` 名额超限 (`enforce_subheartflow_limits`) 或随机停用 (`randomly_deactivate_subflows`) 而将其设置为 `ABSENT` - `SubHeartflowManager``FOCUSED` 名额超限 (`enforce_subheartflow_limits`) 或随机停用 (`randomly_deactivate_subflows`) 而将其设置为 `ABSENT`
- **`CHAT` -> `ABSENT` (退出闲聊)**: - **`CHAT` -> `ABSENT` (退出闲聊)**:
- **主要途径 (内部驱动)**: `SubHeartflowManager` 中的 `evaluate_and_transition_subflows_by_llm` 方法调用 LLM。LLM 读取群聊内容和结合自身状态,判断是否"不想"继续在此群闲聊。 - **主要途径 (内部驱动)**: `SubHeartflowManager` 中的 `sbhf_absent_into_chat` 方法调用 LLM。LLM 读取群聊内容和结合自身状态,判断是否"不想"继续在此群闲聊。
- **执行**: 若 LLM 判断为是,`SubHeartflowManager` 调用 `change_chat_state(ChatState.ABSENT)` - **执行**: 若 LLM 判断为是,`SubHeartflowManager` 调用 `change_chat_state(ChatState.ABSENT)`
- **其他途径 (外部驱动)**: - **其他途径 (外部驱动)**:
- `Heartflow` 主状态变为 `OFFLINE` - `Heartflow` 主状态变为 `OFFLINE`

View File

@@ -12,10 +12,17 @@ from src.heart_flow.interest_logger import InterestLogger
logger = get_logger("background_tasks") logger = get_logger("background_tasks")
# 新增随机停用间隔 (5 分钟)
RANDOM_DEACTIVATION_INTERVAL_SECONDS = 300
# 新增兴趣评估间隔 # 新增兴趣评估间隔
INTEREST_EVAL_INTERVAL_SECONDS = 5 INTEREST_EVAL_INTERVAL_SECONDS = 5
# 新增聊天超时检查间隔
NORMAL_CHAT_TIMEOUT_CHECK_INTERVAL_SECONDS = 60
# 新增状态评估间隔
HF_JUDGE_STATE_UPDATE_INTERVAL_SECONDS = 60
CLEANUP_INTERVAL_SECONDS = 1200
STATE_UPDATE_INTERVAL_SECONDS = 60
LOG_INTERVAL_SECONDS = 3
class BackgroundTaskManager: class BackgroundTaskManager:
@@ -27,33 +34,19 @@ class BackgroundTaskManager:
mai_state_manager: MaiStateManager, mai_state_manager: MaiStateManager,
subheartflow_manager: SubHeartflowManager, subheartflow_manager: SubHeartflowManager,
interest_logger: InterestLogger, interest_logger: InterestLogger,
update_interval: int,
cleanup_interval: int,
log_interval: int,
# 新增兴趣评估间隔参数
interest_eval_interval: int = INTEREST_EVAL_INTERVAL_SECONDS,
# 新增随机停用间隔参数
random_deactivation_interval: int = RANDOM_DEACTIVATION_INTERVAL_SECONDS,
): ):
self.mai_state_info = mai_state_info self.mai_state_info = mai_state_info
self.mai_state_manager = mai_state_manager self.mai_state_manager = mai_state_manager
self.subheartflow_manager = subheartflow_manager self.subheartflow_manager = subheartflow_manager
self.interest_logger = interest_logger self.interest_logger = interest_logger
# Intervals
self.update_interval = update_interval
self.cleanup_interval = cleanup_interval
self.log_interval = log_interval
self.interest_eval_interval = interest_eval_interval # 存储兴趣评估间隔
self.random_deactivation_interval = random_deactivation_interval # 存储随机停用间隔
# Task references # Task references
self._state_update_task: Optional[asyncio.Task] = None self._state_update_task: Optional[asyncio.Task] = None
self._cleanup_task: Optional[asyncio.Task] = None self._cleanup_task: Optional[asyncio.Task] = None
self._logging_task: Optional[asyncio.Task] = None self._logging_task: Optional[asyncio.Task] = None
self._interest_eval_task: Optional[asyncio.Task] = None # 新增兴趣评估任务引用 self._normal_chat_timeout_check_task: Optional[asyncio.Task] = None # Nyaa~ 添加聊天超时检查任务引用
self._random_deactivation_task: Optional[asyncio.Task] = None # 新增随机停用任务引用 self._hf_judge_state_update_task: Optional[asyncio.Task] = None # Nyaa~ 添加状态评估任务引用
self._hf_judge_state_update_task: Optional[asyncio.Task] = None # 新增状态评估任务引用 self._into_focus_task: Optional[asyncio.Task] = None # Nyaa~ 添加兴趣评估任务引用
self._tasks: List[Optional[asyncio.Task]] = [] # Keep track of all tasks self._tasks: List[Optional[asyncio.Task]] = [] # Keep track of all tasks
async def start_tasks(self): async def start_tasks(self):
@@ -65,57 +58,53 @@ class BackgroundTaskManager:
- 将任务引用保存到任务列表 - 将任务引用保存到任务列表
""" """
# 任务配置列表: (任务变量名, 任务函数, 任务名称, 日志级别, 额外日志信息, 任务对象引用属性名) # 任务配置列表: (任务函数, 任务名称, 日志级别, 额外日志信息, 任务对象引用属性名)
task_configs = [ task_configs = [
( (
self._state_update_task, lambda: self._run_state_update_cycle(STATE_UPDATE_INTERVAL_SECONDS),
lambda: self._run_state_update_cycle(self.update_interval),
"hf_state_update",
"debug", "debug",
f"聊天状态更新任务已启动 间隔:{self.update_interval}s", f"聊天状态更新任务已启动 间隔:{STATE_UPDATE_INTERVAL_SECONDS}s",
"_state_update_task", "_state_update_task",
), ),
( (
self._hf_judge_state_update_task, lambda: self._run_normal_chat_timeout_check_cycle(NORMAL_CHAT_TIMEOUT_CHECK_INTERVAL_SECONDS),
lambda: self._run_hf_judge_state_update_cycle(60),
"hf_judge_state_update",
"debug", "debug",
f"状态评估任务已启动 间隔:{60}s", f"聊天超时检查任务已启动 间隔:{NORMAL_CHAT_TIMEOUT_CHECK_INTERVAL_SECONDS}s",
"_normal_chat_timeout_check_task",
),
(
lambda: self._run_absent_into_chat(HF_JUDGE_STATE_UPDATE_INTERVAL_SECONDS),
"debug",
f"状态评估任务已启动 间隔:{HF_JUDGE_STATE_UPDATE_INTERVAL_SECONDS}s",
"_hf_judge_state_update_task", "_hf_judge_state_update_task",
), ),
( (
self._cleanup_task,
self._run_cleanup_cycle, self._run_cleanup_cycle,
"hf_cleanup",
"info", "info",
f"清理任务已启动 间隔:{self.cleanup_interval}s", f"清理任务已启动 间隔:{CLEANUP_INTERVAL_SECONDS}s",
"_cleanup_task", "_cleanup_task",
), ),
( (
self._logging_task,
self._run_logging_cycle, self._run_logging_cycle,
"hf_logging",
"info", "info",
f"日志任务已启动 间隔:{self.log_interval}s", f"日志任务已启动 间隔:{LOG_INTERVAL_SECONDS}s",
"_logging_task", "_logging_task",
), ),
# 新增兴趣评估任务配置 # 新增兴趣评估任务配置
( (
self._interest_eval_task, self._run_into_focus_cycle,
self._run_interest_eval_cycle,
"hf_interest_eval",
"debug", # 设为debug避免过多日志 "debug", # 设为debug避免过多日志
f"兴趣评估任务已启动 间隔:{self.interest_eval_interval}s", f"专注评估任务已启动 间隔:{INTEREST_EVAL_INTERVAL_SECONDS}s",
"_interest_eval_task", "_into_focus_task",
), ),
] ]
# 统一启动所有任务 # 统一启动所有任务
for _task_var, task_func, task_name, log_level, log_msg, task_attr_name in task_configs: for task_func, log_level, log_msg, task_attr_name in task_configs:
# 检查任务变量是否存在且未完成 # 检查任务变量是否存在且未完成
current_task_var = getattr(self, task_attr_name) current_task_var = getattr(self, task_attr_name)
if current_task_var is None or current_task_var.done(): if current_task_var is None or current_task_var.done():
new_task = asyncio.create_task(task_func(), name=task_name) new_task = asyncio.create_task(task_func())
setattr(self, task_attr_name, new_task) # 更新任务变量 setattr(self, task_attr_name, new_task) # 更新任务变量
if new_task not in self._tasks: # 避免重复添加 if new_task not in self._tasks: # 避免重复添加
self._tasks.append(new_task) self._tasks.append(new_task)
@@ -123,7 +112,7 @@ class BackgroundTaskManager:
# 根据配置记录不同级别的日志 # 根据配置记录不同级别的日志
getattr(logger, log_level)(log_msg) getattr(logger, log_level)(log_msg)
else: else:
logger.warning(f"{task_name}任务已在运行") logger.warning(f"{task_attr_name}任务已在运行")
async def stop_tasks(self): async def stop_tasks(self):
"""停止所有后台任务。 """停止所有后台任务。
@@ -209,10 +198,15 @@ class BackgroundTaskManager:
logger.info("检测到离线,停用所有子心流") logger.info("检测到离线,停用所有子心流")
await self.subheartflow_manager.deactivate_all_subflows() await self.subheartflow_manager.deactivate_all_subflows()
async def _perform_hf_judge_state_update_work(self): async def _perform_absent_into_chat(self):
"""调用llm检测是否转换ABSENT-CHAT状态""" """调用llm检测是否转换ABSENT-CHAT状态"""
logger.info("[状态评估任务] 开始基于LLM评估子心流状态...") logger.debug("[状态评估任务] 开始基于LLM评估子心流状态...")
await self.subheartflow_manager.evaluate_and_transition_subflows_by_llm() await self.subheartflow_manager.sbhf_absent_into_chat()
async def _normal_chat_timeout_check_work(self):
"""检查处于CHAT状态的子心流是否因长时间未发言而超时并将其转为ABSENT"""
logger.debug("[聊天超时检查] 开始检查处于CHAT状态的子心流...")
await self.subheartflow_manager.sbhf_chat_into_absent()
async def _perform_cleanup_work(self): async def _perform_cleanup_work(self):
"""执行子心流清理任务 """执行子心流清理任务
@@ -244,10 +238,10 @@ class BackgroundTaskManager:
await self.interest_logger.log_all_states() await self.interest_logger.log_all_states()
# --- 新增兴趣评估工作函数 --- # --- 新增兴趣评估工作函数 ---
async def _perform_interest_eval_work(self): async def _perform_into_focus_work(self):
"""执行一轮子心流兴趣评估与提升检查。""" """执行一轮子心流兴趣评估与提升检查。"""
# 直接调用 subheartflow_manager 的方法,并传递当前状态信息 # 直接调用 subheartflow_manager 的方法,并传递当前状态信息
await self.subheartflow_manager.evaluate_interest_and_promote() await self.subheartflow_manager.sbhf_absent_into_focus()
# --- 结束新增 --- # --- 结束新增 ---
@@ -259,25 +253,30 @@ class BackgroundTaskManager:
task_name="State Update", interval=interval, task_func=self._perform_state_update_work task_name="State Update", interval=interval, task_func=self._perform_state_update_work
) )
async def _run_hf_judge_state_update_cycle(self, interval: int): async def _run_absent_into_chat(self, interval: int):
await self._run_periodic_loop( await self._run_periodic_loop(
task_name="State Update", interval=interval, task_func=self._perform_hf_judge_state_update_work task_name="Into Chat", interval=interval, task_func=self._perform_absent_into_chat
)
async def _run_normal_chat_timeout_check_cycle(self, interval: int):
await self._run_periodic_loop(
task_name="Normal Chat Timeout Check", interval=interval, task_func=self._normal_chat_timeout_check_work
) )
async def _run_cleanup_cycle(self): async def _run_cleanup_cycle(self):
await self._run_periodic_loop( await self._run_periodic_loop(
task_name="Subflow Cleanup", interval=self.cleanup_interval, task_func=self._perform_cleanup_work task_name="Subflow Cleanup", interval=CLEANUP_INTERVAL_SECONDS, task_func=self._perform_cleanup_work
) )
async def _run_logging_cycle(self): async def _run_logging_cycle(self):
await self._run_periodic_loop( await self._run_periodic_loop(
task_name="State Logging", interval=self.log_interval, task_func=self._perform_logging_work task_name="State Logging", interval=LOG_INTERVAL_SECONDS, task_func=self._perform_logging_work
) )
# --- 新增兴趣评估任务运行器 --- # --- 新增兴趣评估任务运行器 ---
async def _run_interest_eval_cycle(self): async def _run_into_focus_cycle(self):
await self._run_periodic_loop( await self._run_periodic_loop(
task_name="Interest Evaluation", task_name="Into Focus",
interval=self.interest_eval_interval, interval=INTEREST_EVAL_INTERVAL_SECONDS,
task_func=self._perform_interest_eval_work, task_func=self._perform_into_focus_work,
) )

View File

@@ -11,20 +11,10 @@ from src.heart_flow.subheartflow_manager import SubHeartflowManager
from src.heart_flow.mind import Mind from src.heart_flow.mind import Mind
from src.heart_flow.interest_logger import InterestLogger # Import InterestLogger from src.heart_flow.interest_logger import InterestLogger # Import InterestLogger
from src.heart_flow.background_tasks import BackgroundTaskManager # Import BackgroundTaskManager from src.heart_flow.background_tasks import BackgroundTaskManager # Import BackgroundTaskManager
# --- End import ---
logger = get_logger("heartflow") logger = get_logger("heartflow")
# Task Intervals (should be in BackgroundTaskManager or config)
CLEANUP_INTERVAL_SECONDS = 1200
STATE_UPDATE_INTERVAL_SECONDS = 60
# Thresholds (should be in SubHeartflowManager or config)
INACTIVE_THRESHOLD_SECONDS = 1200
# --- End Constants --- #
class Heartflow: class Heartflow:
"""主心流协调器,负责初始化并协调各个子系统: """主心流协调器,负责初始化并协调各个子系统:
- 状态管理 (MaiState) - 状态管理 (MaiState)
@@ -65,9 +55,6 @@ class Heartflow:
mai_state_manager=self.mai_state_manager, mai_state_manager=self.mai_state_manager,
subheartflow_manager=self.subheartflow_manager, subheartflow_manager=self.subheartflow_manager,
interest_logger=self.interest_logger, interest_logger=self.interest_logger,
update_interval=STATE_UPDATE_INTERVAL_SECONDS,
cleanup_interval=CLEANUP_INTERVAL_SECONDS,
log_interval=3, # Example: Using value directly, ideally get from config
) )
async def get_or_create_subheartflow(self, subheartflow_id: Any) -> Optional["SubHeartflow"]: async def get_or_create_subheartflow(self, subheartflow_id: Any) -> Optional["SubHeartflow"]:

View File

@@ -4,24 +4,33 @@ import random
from typing import List, Tuple, Optional from typing import List, Tuple, Optional
from src.common.logger_manager import get_logger from src.common.logger_manager import get_logger
from src.plugins.moods.moods import MoodManager from src.plugins.moods.moods import MoodManager
from src.config.config import global_config
logger = get_logger("mai_state") logger = get_logger("mai_state")
# -- 状态相关的可配置参数 (可以从 glocal_config 加载) -- # -- 状态相关的可配置参数 (可以从 glocal_config 加载) --
enable_unlimited_hfc_chat = True # 调试用:无限专注聊天 # The line `enable_unlimited_hfc_chat = False` is setting a configuration parameter that controls
# enable_unlimited_hfc_chat = False # whether a specific debugging feature is enabled or not. When `enable_unlimited_hfc_chat` is set to
prevent_offline_state = True # 调试用:防止进入离线状态 # `False`, it means that the debugging feature for unlimited focused chatting is disabled.
# enable_unlimited_hfc_chat = True # 调试用:无限专注聊天
enable_unlimited_hfc_chat = False
prevent_offline_state = True
# 目前默认不启用OFFLINE状态
# 不同状态下普通聊天的最大消息数 # 不同状态下普通聊天的最大消息数
MAX_NORMAL_CHAT_NUM_PEEKING = 30 base_normal_chat_num = global_config.base_normal_chat_num
MAX_NORMAL_CHAT_NUM_NORMAL = 40 base_focused_chat_num = global_config.base_focused_chat_num
MAX_NORMAL_CHAT_NUM_FOCUSED = 30
MAX_NORMAL_CHAT_NUM_PEEKING = int(base_normal_chat_num / 2)
MAX_NORMAL_CHAT_NUM_NORMAL = base_normal_chat_num
MAX_NORMAL_CHAT_NUM_FOCUSED = base_normal_chat_num + 1
# 不同状态下专注聊天的最大消息数 # 不同状态下专注聊天的最大消息数
MAX_FOCUSED_CHAT_NUM_PEEKING = 20 MAX_FOCUSED_CHAT_NUM_PEEKING = int(base_focused_chat_num / 2)
MAX_FOCUSED_CHAT_NUM_NORMAL = 30 MAX_FOCUSED_CHAT_NUM_NORMAL = base_focused_chat_num
MAX_FOCUSED_CHAT_NUM_FOCUSED = 40 MAX_FOCUSED_CHAT_NUM_FOCUSED = base_focused_chat_num + 2
# -- 状态定义 -- # -- 状态定义 --
@@ -164,7 +173,7 @@ class MaiStateManager:
if random.random() < 0.03: # 3% 概率切换到 OFFLINE if random.random() < 0.03: # 3% 概率切换到 OFFLINE
potential_next = MaiState.OFFLINE potential_next = MaiState.OFFLINE
resolved_next = _resolve_offline(potential_next) resolved_next = _resolve_offline(potential_next)
logger.debug(f"规则1概率触发下线resolve 为 {resolved_next.value}") logger.debug(f"概率触发下线resolve 为 {resolved_next.value}")
# 只有当解析后的状态与当前状态不同时才设置 next_state # 只有当解析后的状态与当前状态不同时才设置 next_state
if resolved_next != current_status: if resolved_next != current_status:
next_state = resolved_next next_state = resolved_next

View File

@@ -146,7 +146,7 @@ class ChattingObservation(Observation):
self.talking_message_str = await build_readable_messages( self.talking_message_str = await build_readable_messages(
messages=self.talking_message, messages=self.talking_message,
timestamp_mode="normal", timestamp_mode="lite",
read_mark=last_obs_time_mark, read_mark=last_obs_time_mark,
) )
self.talking_message_str_truncate = await build_readable_messages( self.talking_message_str_truncate = await build_readable_messages(

View File

@@ -5,7 +5,6 @@ import time
from typing import Optional, List, Dict, Tuple, Callable, Coroutine from typing import Optional, List, Dict, Tuple, Callable, Coroutine
import traceback import traceback
from src.common.logger_manager import get_logger from src.common.logger_manager import get_logger
import random
from src.plugins.chat.message import MessageRecv from src.plugins.chat.message import MessageRecv
from src.plugins.chat.chat_stream import chat_manager from src.plugins.chat.chat_stream import chat_manager
import math import math
@@ -15,20 +14,15 @@ from src.heart_flow.mai_state_manager import MaiStateInfo
from src.heart_flow.chat_state_info import ChatState, ChatStateInfo from src.heart_flow.chat_state_info import ChatState, ChatStateInfo
from src.heart_flow.sub_mind import SubMind from src.heart_flow.sub_mind import SubMind
# # --- REMOVE: Conditional import --- #
# if TYPE_CHECKING:
# from src.heart_flow.subheartflow_manager import SubHeartflowManager
# # --- END REMOVE --- #
# 定义常量 (从 interest.py 移动过来) # 定义常量 (从 interest.py 移动过来)
MAX_INTEREST = 15.0 MAX_INTEREST = 15.0
logger = get_logger("subheartflow") logger = get_logger("sub_heartflow")
base_reply_probability = 0.05 PROBABILITY_INCREASE_RATE_PER_SECOND = 0.1
probability_increase_rate_per_second = 0.08 PROBABILITY_DECREASE_RATE_PER_SECOND = 0.1
max_reply_probability = 1 MAX_REPLY_PROBABILITY = 1
class InterestChatting: class InterestChatting:
@@ -37,24 +31,15 @@ class InterestChatting:
decay_rate=global_config.default_decay_rate_per_second, decay_rate=global_config.default_decay_rate_per_second,
max_interest=MAX_INTEREST, max_interest=MAX_INTEREST,
trigger_threshold=global_config.reply_trigger_threshold, trigger_threshold=global_config.reply_trigger_threshold,
base_reply_probability=base_reply_probability, max_probability=MAX_REPLY_PROBABILITY,
increase_rate=probability_increase_rate_per_second,
decay_factor=global_config.probability_decay_factor_per_second,
max_probability=max_reply_probability,
): ):
# 基础属性初始化 # 基础属性初始化
self.interest_level: float = 0.0 self.interest_level: float = 0.0
self.last_update_time: float = time.time()
self.decay_rate_per_second: float = decay_rate self.decay_rate_per_second: float = decay_rate
self.max_interest: float = max_interest self.max_interest: float = max_interest
self.last_interaction_time: float = self.last_update_time
self.trigger_threshold: float = trigger_threshold self.trigger_threshold: float = trigger_threshold
self.base_reply_probability: float = base_reply_probability
self.probability_increase_rate: float = increase_rate
self.probability_decay_factor: float = decay_factor
self.max_reply_probability: float = max_probability self.max_reply_probability: float = max_probability
self.current_reply_probability: float = 0.0
self.is_above_threshold: bool = False self.is_above_threshold: bool = False
# 任务相关属性初始化 # 任务相关属性初始化
@@ -100,7 +85,6 @@ class InterestChatting:
""" """
# 添加新消息 # 添加新消息
self.interest_dict[message.message_info.message_id] = (message, interest_value, is_mentioned) self.interest_dict[message.message_info.message_id] = (message, interest_value, is_mentioned)
self.last_interaction_time = time.time()
# 如果字典长度超过10删除最旧的消息 # 如果字典长度超过10删除最旧的消息
if len(self.interest_dict) > 10: if len(self.interest_dict) > 10:
@@ -144,10 +128,10 @@ class InterestChatting:
async def _update_reply_probability(self): async def _update_reply_probability(self):
self.above_threshold = self.interest_level >= self.trigger_threshold self.above_threshold = self.interest_level >= self.trigger_threshold
if self.above_threshold: if self.above_threshold:
self.start_hfc_probability += 0.1 self.start_hfc_probability += PROBABILITY_INCREASE_RATE_PER_SECOND
else: else:
if self.start_hfc_probability > 0: if self.start_hfc_probability > 0:
self.start_hfc_probability = max(0, self.start_hfc_probability - 0.1) self.start_hfc_probability = max(0, self.start_hfc_probability - PROBABILITY_DECREASE_RATE_PER_SECOND)
async def increase_interest(self, value: float): async def increase_interest(self, value: float):
self.interest_level += value self.interest_level += value
@@ -168,13 +152,6 @@ class InterestChatting:
"above_threshold": self.above_threshold, "above_threshold": self.above_threshold,
} }
async def should_evaluate_reply(self) -> bool:
if self.current_reply_probability > 0:
trigger = random.random() < self.current_reply_probability
return trigger
else:
return False
# --- 新增后台更新任务相关方法 --- # --- 新增后台更新任务相关方法 ---
async def _run_update_loop(self, update_interval: float = 1.0): async def _run_update_loop(self, update_interval: float = 1.0):
"""后台循环,定期更新兴趣和回复概率。""" """后台循环,定期更新兴趣和回复概率。"""
@@ -322,7 +299,7 @@ class SubHeartflow:
chat_stream = chat_manager.get_stream(self.chat_id) chat_stream = chat_manager.get_stream(self.chat_id)
self.normal_chat_instance = NormalChat(chat_stream=chat_stream, interest_dict=self.get_interest_dict()) self.normal_chat_instance = NormalChat(chat_stream=chat_stream, interest_dict=self.get_interest_dict())
logger.info(f"{log_prefix} 启动 NormalChat 随便水群...") logger.info(f"{log_prefix} 开始普通聊天,随便水群...")
await self.normal_chat_instance.start_chat() # <--- 修正:调用 start_chat await self.normal_chat_instance.start_chat() # <--- 修正:调用 start_chat
return True return True
except Exception as e: except Exception as e:
@@ -334,7 +311,7 @@ class SubHeartflow:
async def _stop_heart_fc_chat(self): async def _stop_heart_fc_chat(self):
"""停止并清理 HeartFChatting 实例""" """停止并清理 HeartFChatting 实例"""
if self.heart_fc_instance: if self.heart_fc_instance:
logger.info(f"{self.log_prefix} 关闭 HeartFChatting 实例...") logger.debug(f"{self.log_prefix} 结束专注聊天...")
try: try:
await self.heart_fc_instance.shutdown() await self.heart_fc_instance.shutdown()
except Exception as e: except Exception as e:
@@ -369,7 +346,7 @@ class SubHeartflow:
return True # 已经在运行 return True # 已经在运行
# 如果实例不存在,则创建并启动 # 如果实例不存在,则创建并启动
logger.info(f"{log_prefix} 麦麦准备开始专注聊天 (创建新实例)...") logger.info(f"{log_prefix} 麦麦准备开始专注聊天...")
try: try:
# 创建 HeartFChatting 实例,并传递 从构造函数传入的 回调函数 # 创建 HeartFChatting 实例,并传递 从构造函数传入的 回调函数
self.heart_fc_instance = HeartFChatting( self.heart_fc_instance = HeartFChatting(
@@ -382,7 +359,7 @@ class SubHeartflow:
# 初始化并启动 HeartFChatting # 初始化并启动 HeartFChatting
if await self.heart_fc_instance._initialize(): if await self.heart_fc_instance._initialize():
await self.heart_fc_instance.start() await self.heart_fc_instance.start()
logger.info(f"{log_prefix} 麦麦已成功进入专注聊天模式 (新实例已启动)。") logger.debug(f"{log_prefix} 麦麦已成功进入专注聊天模式 (新实例已启动)。")
return True return True
else: else:
logger.error(f"{log_prefix} HeartFChatting 初始化失败,无法进入专注模式。") logger.error(f"{log_prefix} HeartFChatting 初始化失败,无法进入专注模式。")
@@ -409,7 +386,7 @@ class SubHeartflow:
# 移除限额检查逻辑 # 移除限额检查逻辑
logger.debug(f"{log_prefix} 准备进入或保持 聊天 状态") logger.debug(f"{log_prefix} 准备进入或保持 聊天 状态")
if await self._start_normal_chat(): if await self._start_normal_chat():
logger.info(f"{log_prefix} 成功进入或保持 NormalChat 状态。") # logger.info(f"{log_prefix} 成功进入或保持 NormalChat 状态。")
state_changed = True state_changed = True
else: else:
logger.error(f"{log_prefix} 启动 NormalChat 失败,无法进入 CHAT 状态。") logger.error(f"{log_prefix} 启动 NormalChat 失败,无法进入 CHAT 状态。")
@@ -420,7 +397,7 @@ class SubHeartflow:
# 移除限额检查逻辑 # 移除限额检查逻辑
logger.debug(f"{log_prefix} 准备进入或保持 专注聊天 状态") logger.debug(f"{log_prefix} 准备进入或保持 专注聊天 状态")
if await self._start_heart_fc_chat(): if await self._start_heart_fc_chat():
logger.info(f"{log_prefix} 成功进入或保持 HeartFChatting 状态。") logger.debug(f"{log_prefix} 成功进入或保持 HeartFChatting 状态。")
state_changed = True state_changed = True
else: else:
logger.error(f"{log_prefix} 启动 HeartFChatting 失败,无法进入 FOCUSED 状态。") logger.error(f"{log_prefix} 启动 HeartFChatting 失败,无法进入 FOCUSED 状态。")
@@ -439,7 +416,7 @@ class SubHeartflow:
self.history_chat_state.append((current_state, self.chat_state_last_time)) self.history_chat_state.append((current_state, self.chat_state_last_time))
logger.info( logger.info(
f"{log_prefix} 麦麦的聊天状态从 {current_state.value} (持续了 {self.chat_state_last_time} 秒) 变更为 {new_state.value}" f"{log_prefix} 麦麦的聊天状态从 {current_state.value} (持续了 {int(self.chat_state_last_time)} 秒) 变更为 {new_state.value}"
) )
self.chat_state.chat_status = new_state self.chat_state.chat_status = new_state
@@ -493,11 +470,10 @@ class SubHeartflow:
async def get_interest_state(self) -> dict: async def get_interest_state(self) -> dict:
return await self.interest_chatting.get_state() return await self.interest_chatting.get_state()
async def get_interest_level(self) -> float: def get_normal_chat_last_speak_time(self) -> float:
return await self.interest_chatting.get_interest() if self.normal_chat_instance:
return self.normal_chat_instance.last_speak_time
async def should_evaluate_reply(self) -> bool: return 0
return await self.interest_chatting.should_evaluate_reply()
def get_interest_dict(self) -> Dict[str, tuple[MessageRecv, float, bool]]: def get_interest_dict(self) -> Dict[str, tuple[MessageRecv, float, bool]]:
return self.interest_chatting.interest_dict return self.interest_chatting.interest_dict
@@ -535,12 +511,12 @@ class SubHeartflow:
# 取消可能存在的旧后台任务 (self.task) # 取消可能存在的旧后台任务 (self.task)
if self.task and not self.task.done(): if self.task and not self.task.done():
logger.info(f"{self.log_prefix} 取消子心流主任务 (Shutdown)...") logger.debug(f"{self.log_prefix} 取消子心流主任务 (Shutdown)...")
self.task.cancel() self.task.cancel()
try: try:
await asyncio.wait_for(self.task, timeout=1.0) # 给点时间响应取消 await asyncio.wait_for(self.task, timeout=1.0) # 给点时间响应取消
except asyncio.CancelledError: except asyncio.CancelledError:
logger.info(f"{self.log_prefix} 子心流主任务已取消 (Shutdown)。") logger.debug(f"{self.log_prefix} 子心流主任务已取消 (Shutdown)。")
except asyncio.TimeoutError: except asyncio.TimeoutError:
logger.warning(f"{self.log_prefix} 等待子心流主任务取消超时 (Shutdown)。") logger.warning(f"{self.log_prefix} 等待子心流主任务取消超时 (Shutdown)。")
except Exception as e: except Exception as e:

View File

@@ -140,11 +140,11 @@ class SubMind:
individuality = Individuality.get_instance() individuality = Individuality.get_instance()
relation_prompt = "" relation_prompt = ""
print(f"person_list: {person_list}") # print(f"person_list: {person_list}")
for person in person_list: for person in person_list:
relation_prompt += await relationship_manager.build_relationship_info(person, is_id=True) relation_prompt += await relationship_manager.build_relationship_info(person, is_id=True)
print(f"relat22222ion_prompt: {relation_prompt}") # print(f"relat22222ion_prompt: {relation_prompt}")
# 构建个性部分 # 构建个性部分
prompt_personality = individuality.get_prompt(x_person=2, level=2) prompt_personality = individuality.get_prompt(x_person=2, level=2)

View File

@@ -1,7 +1,7 @@
import asyncio import asyncio
import time import time
import random import random
from typing import Dict, Any, Optional, List from typing import Dict, Any, Optional, List, Tuple
import json # 导入 json 模块 import json # 导入 json 模块
import functools # <-- 新增导入 import functools # <-- 新增导入
@@ -29,6 +29,7 @@ logger = get_logger("subheartflow_manager")
# 子心流管理相关常量 # 子心流管理相关常量
INACTIVE_THRESHOLD_SECONDS = 3600 # 子心流不活跃超时时间(秒) INACTIVE_THRESHOLD_SECONDS = 3600 # 子心流不活跃超时时间(秒)
NORMAL_CHAT_TIMEOUT_SECONDS = 30 * 60 # 30分钟
class SubHeartflowManager: class SubHeartflowManager:
@@ -256,7 +257,7 @@ class SubHeartflowManager:
f"{log_prefix} 完成,共处理 {processed_count} 个子心流,成功将 {changed_count} 个非 ABSENT 子心流的状态更改为 ABSENT。" f"{log_prefix} 完成,共处理 {processed_count} 个子心流,成功将 {changed_count} 个非 ABSENT 子心流的状态更改为 ABSENT。"
) )
async def evaluate_interest_and_promote(self): async def sbhf_absent_into_focus(self):
"""评估子心流兴趣度满足条件且未达上限则提升到FOCUSED状态基于start_hfc_probability""" """评估子心流兴趣度满足条件且未达上限则提升到FOCUSED状态基于start_hfc_probability"""
try: try:
log_prefix = "[兴趣评估]" log_prefix = "[兴趣评估]"
@@ -271,10 +272,7 @@ class SubHeartflowManager:
return # 如果不允许,直接返回 return # 如果不允许,直接返回
# --- 结束新增 --- # --- 结束新增 ---
logger.debug(f"{log_prefix} 当前状态 ({current_state.value}) 开始尝试提升到FOCUSED状态") logger.debug(f"{log_prefix} 当前状态 ({current_state.value}) 可以在{focused_limit}个群激情聊天")
if int(time.time()) % 20 == 0: # 每20秒输出一次
logger.debug(f"{log_prefix} 当前状态 ({current_state.value}) 可以在{focused_limit}个群激情聊天")
if focused_limit <= 0: if focused_limit <= 0:
# logger.debug(f"{log_prefix} 当前状态 ({current_state.value}) 不允许 FOCUSED 子心流") # logger.debug(f"{log_prefix} 当前状态 ({current_state.value}) 不允许 FOCUSED 子心流")
@@ -333,139 +331,207 @@ class SubHeartflowManager:
except Exception as e: except Exception as e:
logger.error(f"启动HFC 兴趣评估失败: {e}", exc_info=True) logger.error(f"启动HFC 兴趣评估失败: {e}", exc_info=True)
async def evaluate_and_transition_subflows_by_llm(self): async def sbhf_absent_into_chat(self):
""" """
使用LLM评估每个子心流的状态并根据LLM的判断执行状态转换ABSENT <-> CHAT 随机选一个 ABSENT 状态的子心流,评估是否应转换为 CHAT 状态
注意此函数包含对假设的LLM函数的调用 每次调用最多转换一个
""" """
# 获取当前状态和限制用于CHAT激活检查
current_mai_state = self.mai_state_info.get_current_state() current_mai_state = self.mai_state_info.get_current_state()
chat_limit = current_mai_state.get_normal_chat_max_num() chat_limit = current_mai_state.get_normal_chat_max_num()
transitioned_to_chat = 0 async with self._lock:
transitioned_to_absent = 0 # 1. 筛选出所有 ABSENT 状态的子心流
absent_subflows = [
hf for hf in self.subheartflows.values() if hf.chat_state.chat_status == ChatState.ABSENT
]
async with self._lock: # 在锁内获取快照并迭代 if not absent_subflows:
subflows_snapshot = list(self.subheartflows.values()) logger.debug("没有摸鱼的子心流可以评估。") # 日志太频繁,注释掉
# 使用不上锁的版本,因为我们已经在锁内 return # 没有目标,直接返回
# 2. 随机选一个幸运儿
sub_hf_to_evaluate = random.choice(absent_subflows)
flow_id = sub_hf_to_evaluate.subheartflow_id
stream_name = chat_manager.get_stream_name(flow_id) or flow_id
log_prefix = f"[{stream_name}]"
# 3. 检查 CHAT 上限
current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT) current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT)
if current_chat_count >= chat_limit:
logger.info(f"{log_prefix} 想看看能不能聊,但是聊天太多了, ({current_chat_count}/{chat_limit}) 满了。")
return # 满了,这次就算了
# --- 获取 FOCUSED 计数 ---
current_focused_count = self.count_subflows_by_state_nolock(ChatState.FOCUSED)
focused_limit = current_mai_state.get_focused_chat_max_num()
# --- 新增:获取聊天和专注群名 ---
chatting_group_names = []
focused_group_names = []
for flow_id, hf in self.subheartflows.items():
stream_name = chat_manager.get_stream_name(flow_id) or str(flow_id) # 保证有名字
if hf.chat_state.chat_status == ChatState.CHAT:
chatting_group_names.append(stream_name)
elif hf.chat_state.chat_status == ChatState.FOCUSED:
focused_group_names.append(stream_name)
# --- 结束新增 ---
# --- 获取观察信息和构建 Prompt ---
first_observation = sub_hf_to_evaluate.observations[0] # 喵~第一个观察者肯定存在的说
await first_observation.observe()
current_chat_log = first_observation.talking_message_str or "当前没啥聊天内容。"
_observation_summary = f"最近聊了这些:\n{current_chat_log}"
mai_state_description = f"你当前状态: {current_mai_state.value}"
individuality = Individuality.get_instance()
personality_prompt = individuality.get_prompt(x_person=2, level=2)
prompt_personality = f"你正在扮演名为{individuality.name}的人类,{personality_prompt}"
# --- 修改:在 prompt 中加入当前聊天计数和群名信息 (条件显示) ---
chat_status_lines = []
if chatting_group_names:
chat_status_lines.append(
f"正在闲聊 ({current_chat_count}/{chat_limit}): {', '.join(chatting_group_names)}"
)
if focused_group_names:
chat_status_lines.append(
f"正在专注 ({current_focused_count}/{focused_limit}): {', '.join(focused_group_names)}"
)
chat_status_prompt = "当前没有在任何群聊中。" # 默认消息喵~
if chat_status_lines:
chat_status_prompt = "当前聊天情况:\n" + "\n".join(chat_status_lines) # 拼接状态信息
prompt = (
f"{prompt_personality}\\n"
f"你当前没在 [{stream_name}] 群聊天。\\n"
f"{mai_state_description}\\n"
f"{chat_status_prompt}\\n" # <-- 喵!用了新的状态信息~
f"{_observation_summary}\\n---\\n"
f"基于以上信息,你想不想开始在这个群闲聊?\\n"
f"请说明理由,并以 JSON 格式回答,包含 'decision' (布尔值) 和 'reason' (字符串)。\\n"
f'例如:{{"decision": true, "reason": "看起来挺热闹的,插个话"}}\\n'
f'例如:{{"decision": false, "reason": "已经聊了好多,休息一下"}}\\n'
f"请只输出有效的 JSON 对象。"
)
# --- 结束修改 ---
# --- 4. LLM 评估是否想聊 ---
yao_kai_shi_liao_ma, reason = await self._llm_evaluate_state_transition(prompt)
if reason:
if yao_kai_shi_liao_ma:
logger.info(f"{log_prefix} 打算开始聊,原因是: {reason}")
else:
logger.info(f"{log_prefix} 不打算聊,原因是: {reason}")
else:
logger.info(f"{log_prefix} 结果: {yao_kai_shi_liao_ma}")
if yao_kai_shi_liao_ma is None:
logger.debug(f"{log_prefix} 问AI想不想聊失败了这次算了。")
return # 评估失败,结束
if not yao_kai_shi_liao_ma:
# logger.info(f"{log_prefix} 现在不想聊这个群。")
return # 不想聊,结束
# --- 5. AI想聊再次检查额度并尝试转换 ---
# 再次检查以防万一
current_chat_count_before_change = self.count_subflows_by_state_nolock(ChatState.CHAT)
if current_chat_count_before_change < chat_limit:
logger.info(
f"{log_prefix} 想聊,而且还有精力 ({current_chat_count_before_change}/{chat_limit}),这就去聊!"
)
await sub_hf_to_evaluate.change_chat_state(ChatState.CHAT)
# 确认转换成功
if sub_hf_to_evaluate.chat_state.chat_status == ChatState.CHAT:
logger.debug(f"{log_prefix} 成功进入聊天状态!本次评估圆满结束。")
else:
logger.warning(
f"{log_prefix} 奇怪,尝试进入聊天状态失败了。当前状态: {sub_hf_to_evaluate.chat_state.chat_status.value}"
)
else:
logger.warning(
f"{log_prefix} AI说想聊但是刚问完就没空位了 ({current_chat_count_before_change}/{chat_limit})。真不巧,下次再说吧。"
)
# 无论转换成功与否,本次评估都结束了
# 锁在这里自动释放
# --- 新增:单独检查 CHAT 状态超时的任务 ---
async def sbhf_chat_into_absent(self):
"""定期检查处于 CHAT 状态的子心流是否因长时间未发言而超时,并将其转为 ABSENT。"""
log_prefix_task = "[聊天超时检查]"
transitioned_to_absent = 0
checked_count = 0
async with self._lock:
subflows_snapshot = list(self.subheartflows.values())
checked_count = len(subflows_snapshot)
if not subflows_snapshot: if not subflows_snapshot:
logger.info("当前没有子心流需要评估。") # logger.debug(f"{log_prefix_task} 没有子心流需要检查超时。")
return return
for sub_hf in subflows_snapshot: for sub_hf in subflows_snapshot:
# 只检查 CHAT 状态的子心流
if sub_hf.chat_state.chat_status != ChatState.CHAT:
continue
flow_id = sub_hf.subheartflow_id flow_id = sub_hf.subheartflow_id
stream_name = chat_manager.get_stream_name(flow_id) or flow_id stream_name = chat_manager.get_stream_name(flow_id) or flow_id
log_prefix = f"[{stream_name}]" log_prefix = f"[{stream_name}]({log_prefix_task})"
current_subflow_state = sub_hf.chat_state.chat_status
_observation_summary = "没有可用的观察信息。" # 默认值 should_deactivate = False
reason = ""
first_observation = sub_hf.observations[0] try:
if isinstance(first_observation, ChattingObservation): # 使用变量名 last_bot_dong_zuo_time 替代 last_bot_activity_time
# 组合中期记忆和当前聊天内容 last_bot_dong_zuo_time = sub_hf.get_normal_chat_last_speak_time()
await first_observation.observe()
current_chat = first_observation.talking_message_str or "当前无聊天内容。"
combined_summary = f"当前聊天内容:\n{current_chat}"
else:
logger.warning(f"{log_prefix} [{stream_name}] 第一个观察者不是 ChattingObservation 类型。")
# --- 获取麦麦状态 --- if last_bot_dong_zuo_time > 0:
mai_state_description = f"你当前状态: {current_mai_state.value}" current_time = time.time()
# 使用变量名 time_since_last_bb 替代 time_since_last_reply
time_since_last_bb = current_time - last_bot_dong_zuo_time
# 获取个性化信息 if time_since_last_bb > NORMAL_CHAT_TIMEOUT_SECONDS:
individuality = Individuality.get_instance() should_deactivate = True
reason = f"超过 {NORMAL_CHAT_TIMEOUT_SECONDS / 60:.0f} 分钟没 BB"
# 构建个性部分
prompt_personality = f"你正在扮演名为{individuality.personality.bot_nickname}的人类,你"
prompt_personality += individuality.personality.personality_core
# 随机添加个性侧面
if individuality.personality.personality_sides:
random_side = random.choice(individuality.personality.personality_sides)
prompt_personality += f"{random_side}"
# 随机添加身份细节
if individuality.identity.identity_detail:
random_detail = random.choice(individuality.identity.identity_detail)
prompt_personality += f"{random_detail}"
# --- 针对 ABSENT 状态 ---
if current_subflow_state == ChatState.ABSENT:
# 构建Prompt
prompt = (
f"{prompt_personality}\n"
f"你当前没有在: [{stream_name}] 群中聊天。\n"
f"{mai_state_description}\n"
f"这个群里最近的聊天内容是:\n---\n{combined_summary}\n---\n"
f"基于以上信息,请判断你是否愿意在这个群开始闲聊,"
f"进入常规聊天(CHAT)状态?\n"
f"给出你的判断,和理由,然后以 JSON 格式回答"
f"包含键 'decision',如果要开始聊天,值为 true ,否则为 false.\n"
f"包含键 'reason',其值为你的理由。\n"
f'例如:{{"decision": true, "reason": "因为我想聊天"}}\n'
f"请只输出有效的 JSON 对象。"
)
# 调用LLM评估
should_activate = await self._llm_evaluate_state_transition(prompt)
if should_activate is None: # 处理解析失败或意外情况
logger.warning(f"{log_prefix}LLM评估返回无效结果跳过。")
continue
if should_activate:
# 检查CHAT限额
# 使用不上锁的版本,因为我们已经在锁内
current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT)
if current_chat_count < chat_limit:
logger.info( logger.info(
f"{log_prefix}LLM建议激活到CHAT状态且未达上限({current_chat_count}/{chat_limit})。正在尝试转换..." f"{log_prefix} 太久没有发言 ({reason}),不看了。上次活动时间: {last_bot_dong_zuo_time:.0f}"
) )
await sub_hf.change_chat_state(ChatState.CHAT) # else:
if sub_hf.chat_state.chat_status == ChatState.CHAT: # logger.debug(f"{log_prefix} Bot活动时间未超时 ({time_since_last_bb:.0f}s < {NORMAL_CHAT_TIMEOUT_SECONDS}s),保持 CHAT 状态。")
transitioned_to_chat += 1 # else:
else: # 如果没有记录到Bot的活动时间暂时不因为超时而转换状态
logger.warning(f"{log_prefix}尝试激活到CHAT失败。") # logger.debug(f"{log_prefix} 未找到有效的 Bot 最后活动时间记录,不执行超时检查。")
else:
logger.info(
f"{log_prefix}LLM建议激活到CHAT状态但已达到上限({current_chat_count}/{chat_limit})。跳过转换。"
)
else:
logger.info(f"{log_prefix}LLM建议不激活到CHAT状态。")
# --- 针对 CHAT 状态 --- except AttributeError:
elif current_subflow_state == ChatState.CHAT: logger.error(
# 构建Prompt f"{log_prefix} 无法获取 Bot 最后 BB 时间,请确保 SubHeartflow 相关实现正确。跳过超时检查。"
prompt = (
f"{prompt_personality}\n"
f"你正在在: [{stream_name}] 群中聊天。\n"
f"{mai_state_description}\n"
f"这个群里最近的聊天内容是:\n---\n{combined_summary}\n---\n"
f"基于以上信息,请判断你是否愿意在这个群继续闲聊,"
f"还是暂时离开聊天,进入休眠状态?\n"
f"给出你的判断,和理由,然后以 JSON 格式回答"
f"包含键 'decision',如果要离开聊天,值为 true ,否则为 false.\n"
f"包含键 'reason',其值为你的理由。\n"
f'例如:{{"decision": true, "reason": "因为我想休息"}}\n'
f"请只输出有效的 JSON 对象。"
) )
except Exception as e:
logger.error(f"{log_prefix} 检查 Bot 超时状态时出错: {e}", exc_info=True)
# 调用LLM评估 # --- 执行状态转换(如果超时) ---
should_deactivate = await self._llm_evaluate_state_transition(prompt) if should_deactivate:
if should_deactivate is None: # 处理解析失败或意外情况 logger.debug(f"{log_prefix} 因超时 ({reason}),尝试转换为 ABSENT 状态。")
logger.warning(f"{log_prefix}LLM评估返回无效结果跳过。") await sub_hf.change_chat_state(ChatState.ABSENT)
continue # 再次检查确保状态已改变
if sub_hf.chat_state.chat_status == ChatState.ABSENT:
if should_deactivate: transitioned_to_absent += 1
logger.info(f"{log_prefix}LLM建议进入ABSENT状态。正在尝试转换...") logger.info(f"{log_prefix} 不看了。")
await sub_hf.change_chat_state(ChatState.ABSENT)
if sub_hf.chat_state.chat_status == ChatState.ABSENT:
transitioned_to_absent += 1
else: else:
logger.info(f"{log_prefix}LLM建议不进入ABSENT状态") logger.warning(f"{log_prefix} 尝试因超时转换为 ABSENT 失败")
async def _llm_evaluate_state_transition(self, prompt: str) -> Optional[bool]: if transitioned_to_absent > 0:
logger.debug(
f"{log_prefix_task} 完成,共检查 {checked_count} 个子心流,{transitioned_to_absent} 个因超时转为 ABSENT。"
)
# --- 结束新增 ---
async def _llm_evaluate_state_transition(self, prompt: str) -> Tuple[Optional[bool], Optional[str]]:
""" """
使用 LLM 评估是否应进行状态转换,期望 LLM 返回 JSON 格式。 使用 LLM 评估是否应进行状态转换,期望 LLM 返回 JSON 格式。
@@ -482,7 +548,7 @@ class SubHeartflowManager:
response_text, _ = await self.llm_state_evaluator.generate_response_async(prompt) response_text, _ = await self.llm_state_evaluator.generate_response_async(prompt)
# logger.debug(f"{log_prefix} 使用模型 {self.llm_state_evaluator.model_name} 评估") # logger.debug(f"{log_prefix} 使用模型 {self.llm_state_evaluator.model_name} 评估")
logger.debug(f"{log_prefix} 原始输入: {prompt}") logger.debug(f"{log_prefix} 原始输入: {prompt}")
logger.debug(f"{log_prefix} 原始响应: {response_text}") logger.debug(f"{log_prefix} 原始评估结果: {response_text}")
# --- 解析 JSON 响应 --- # --- 解析 JSON 响应 ---
try: try:
@@ -493,34 +559,36 @@ class SubHeartflowManager:
data = json.loads(cleaned_response) data = json.loads(cleaned_response)
decision = data.get("decision") # 使用 .get() 避免 KeyError decision = data.get("decision") # 使用 .get() 避免 KeyError
reason = data.get("reason")
if isinstance(decision, bool): if isinstance(decision, bool):
logger.debug(f"{log_prefix} LLM评估结果 (来自JSON): {'建议转换' if decision else '建议不转换'}") logger.debug(f"{log_prefix} LLM评估结果 (来自JSON): {'建议转换' if decision else '建议不转换'}")
return decision
return decision, reason
else: else:
logger.warning( logger.warning(
f"{log_prefix} LLM 返回的 JSON 中 'decision' 键的值不是布尔型: {decision}。响应: {response_text}" f"{log_prefix} LLM 返回的 JSON 中 'decision' 键的值不是布尔型: {decision}。响应: {response_text}"
) )
return None # 值类型不正确 return None, None # 值类型不正确
except json.JSONDecodeError as json_err: except json.JSONDecodeError as json_err:
logger.warning(f"{log_prefix} LLM 返回的响应不是有效的 JSON: {json_err}。响应: {response_text}") logger.warning(f"{log_prefix} LLM 返回的响应不是有效的 JSON: {json_err}。响应: {response_text}")
# 尝试在非JSON响应中查找关键词作为后备方案 (可选) # 尝试在非JSON响应中查找关键词作为后备方案 (可选)
if "true" in response_text.lower(): if "true" in response_text.lower():
logger.debug(f"{log_prefix} 在非JSON响应中找到 'true',解释为建议转换") logger.debug(f"{log_prefix} 在非JSON响应中找到 'true',解释为建议转换")
return True return True, None
if "false" in response_text.lower(): if "false" in response_text.lower():
logger.debug(f"{log_prefix} 在非JSON响应中找到 'false',解释为建议不转换") logger.debug(f"{log_prefix} 在非JSON响应中找到 'false',解释为建议不转换")
return False return False, None
return None # JSON 解析失败,也未找到关键词 return None, None # JSON 解析失败,也未找到关键词
except Exception as parse_err: # 捕获其他可能的解析错误 except Exception as parse_err: # 捕获其他可能的解析错误
logger.warning(f"{log_prefix} 解析 LLM JSON 响应时发生意外错误: {parse_err}。响应: {response_text}") logger.warning(f"{log_prefix} 解析 LLM JSON 响应时发生意外错误: {parse_err}。响应: {response_text}")
return None return None, None
except Exception as e: except Exception as e:
logger.error(f"{log_prefix} 调用 LLM 或处理其响应时出错: {e}", exc_info=True) logger.error(f"{log_prefix} 调用 LLM 或处理其响应时出错: {e}", exc_info=True)
traceback.print_exc() traceback.print_exc()
return None # LLM 调用或处理失败 return None, None # LLM 调用或处理失败
def count_subflows_by_state(self, state: ChatState) -> int: def count_subflows_by_state(self, state: ChatState) -> int:
"""统计指定状态的子心流数量""" """统计指定状态的子心流数量"""
@@ -579,14 +647,14 @@ class SubHeartflowManager:
# --- 新增:处理 HFC 无回复回调的专用方法 --- # # --- 新增:处理 HFC 无回复回调的专用方法 --- #
async def _handle_hfc_no_reply(self, subheartflow_id: Any): async def _handle_hfc_no_reply(self, subheartflow_id: Any):
"""处理来自 HeartFChatting 的连续无回复信号 (通过 partial 绑定 ID)""" """处理来自 HeartFChatting 的连续无回复信号 (通过 partial 绑定 ID)"""
# 注意:这里不需要再获取锁,因为 request_absent_transition 内部会处理锁 # 注意:这里不需要再获取锁,因为 sbhf_focus_into_absent 内部会处理锁
logger.debug(f"[管理器 HFC 处理器] 接收到来自 {subheartflow_id} 的 HFC 无回复信号") logger.debug(f"[管理器 HFC 处理器] 接收到来自 {subheartflow_id} 的 HFC 无回复信号")
await self.request_absent_transition(subheartflow_id) await self.sbhf_focus_into_absent(subheartflow_id)
# --- 结束新增 --- # # --- 结束新增 --- #
# --- 新增:处理来自 HeartFChatting 的状态转换请求 --- # # --- 新增:处理来自 HeartFChatting 的状态转换请求 --- #
async def request_absent_transition(self, subflow_id: Any): async def sbhf_focus_into_absent(self, subflow_id: Any):
""" """
接收来自 HeartFChatting 的请求,将特定子心流的状态转换为 ABSENT。 接收来自 HeartFChatting 的请求,将特定子心流的状态转换为 ABSENT。
通常在连续多次 "no_reply" 后被调用。 通常在连续多次 "no_reply" 后被调用。
@@ -606,12 +674,52 @@ class SubHeartflowManager:
# 仅当子心流处于 FOCUSED 状态时才进行转换 # 仅当子心流处于 FOCUSED 状态时才进行转换
# 因为 HeartFChatting 只在 FOCUSED 状态下运行 # 因为 HeartFChatting 只在 FOCUSED 状态下运行
if current_state == ChatState.FOCUSED: if current_state == ChatState.FOCUSED:
logger.info(f"[状态转换请求] 接收到请求,将 {stream_name} (当前: {current_state.value}) 转换为 ABSENT") target_state = ChatState.ABSENT # 默认目标状态
log_reason = "默认转换"
# 决定是去 ABSENT 还是 CHAT
if random.random() < 0.5:
target_state = ChatState.ABSENT
log_reason = "随机选择 ABSENT"
logger.debug(f"[状态转换请求] {stream_name} ({current_state.value}) 随机决定进入 ABSENT")
else:
# 尝试进入 CHAT先检查限制
current_mai_state = self.mai_state_info.get_current_state()
chat_limit = current_mai_state.get_normal_chat_max_num()
# 使用不上锁的版本,因为我们已经在锁内
current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT)
if current_chat_count < chat_limit:
target_state = ChatState.CHAT
log_reason = f"随机选择 CHAT (当前 {current_chat_count}/{chat_limit})"
logger.debug(
f"[状态转换请求] {stream_name} ({current_state.value}) 随机决定进入 CHAT未达上限 ({current_chat_count}/{chat_limit})"
)
else:
target_state = ChatState.ABSENT
log_reason = f"随机选择 CHAT 但已达上限 ({current_chat_count}/{chat_limit}),转为 ABSENT"
logger.debug(
f"[状态转换请求] {stream_name} ({current_state.value}) 随机决定进入 CHAT但已达上限 ({current_chat_count}/{chat_limit}),改为进入 ABSENT"
)
# 开始转换
logger.info(
f"[状态转换请求] 接收到请求,将 {stream_name} (当前: {current_state.value}) 尝试转换为 {target_state.value} ({log_reason})"
)
try: try:
await subflow.change_chat_state(ChatState.ABSENT) await subflow.change_chat_state(target_state)
logger.info(f"[状态转换请求] {stream_name} 状态已成功转换为 ABSENT") # 检查最终状态
final_state = subflow.chat_state.chat_status
if final_state == target_state:
logger.debug(f"[状态转换请求] {stream_name} 状态已成功转换为 {final_state.value}")
else:
logger.warning(
f"[状态转换请求] 尝试将 {stream_name} 转换为 {target_state.value} 后,状态实际为 {final_state.value}"
)
except Exception as e: except Exception as e:
logger.error(f"[状态转换请求] 转换 {stream_name} 到 ABSENT 时出错: {e}", exc_info=True) logger.error(
f"[状态转换请求] 转换 {stream_name}{target_state.value} 时出错: {e}", exc_info=True
)
elif current_state == ChatState.ABSENT: elif current_state == ChatState.ABSENT:
logger.debug(f"[状态转换请求] {stream_name} 已处于 ABSENT 状态,无需转换") logger.debug(f"[状态转换请求] {stream_name} 已处于 ABSENT 状态,无需转换")
else: else:

View File

@@ -191,7 +191,7 @@ class Individuality:
获取合并的个体特征prompt 获取合并的个体特征prompt
Args: Args:
level (int): 详细程度 (1: 核心/随机细节, 2: 核心+侧面/细节+其他, 3: 全部) level (int): 详细程度 (1: 核心/随机细节, 2: 核心+随机侧面/全部细节, 3: 全部)
x_person (int, optional): 人称代词 (0: 无人称, 1: 我, 2: 你). 默认为 2. x_person (int, optional): 人称代词 (0: 无人称, 1: 我, 2: 你). 默认为 2.
Returns: Returns:

View File

@@ -21,6 +21,7 @@ PROMPT_INITIAL_REPLY = """{persona_text}。现在你在参与一场QQ私聊
【当前对话目标】 【当前对话目标】
{goals_str} {goals_str}
{knowledge_info_str}
【最近行动历史概要】 【最近行动历史概要】
{action_history_summary} {action_history_summary}
@@ -33,7 +34,7 @@ PROMPT_INITIAL_REPLY = """{persona_text}。现在你在参与一场QQ私聊
------ ------
可选行动类型以及解释: 可选行动类型以及解释:
fetch_knowledge: 需要调取知识,当需要专业知识或特定信息时选择,对方若提到你不太认识的人名或实体也可以尝试选择 fetch_knowledge: 需要调取知识或记忆,当需要专业知识或特定信息时选择,对方若提到你不太认识的人名或实体也可以尝试选择
listening: 倾听对方发言,当你认为对方话才说到一半,发言明显未结束时选择 listening: 倾听对方发言,当你认为对方话才说到一半,发言明显未结束时选择
direct_reply: 直接回复对方 direct_reply: 直接回复对方
rethink_goal: 思考一个对话目标,当你觉得目前对话需要目标,或当前目标不再适用,或话题卡住时选择。注意私聊的环境是灵活的,有可能需要经常选择 rethink_goal: 思考一个对话目标,当你觉得目前对话需要目标,或当前目标不再适用,或话题卡住时选择。注意私聊的环境是灵活的,有可能需要经常选择
@@ -53,6 +54,7 @@ PROMPT_FOLLOW_UP = """{persona_text}。现在你在参与一场QQ私聊刚刚
【当前对话目标】 【当前对话目标】
{goals_str} {goals_str}
{knowledge_info_str}
【最近行动历史概要】 【最近行动历史概要】
{action_history_summary} {action_history_summary}
@@ -224,6 +226,41 @@ class ActionPlanner:
logger.error(f"[私聊][{self.private_name}]构建对话目标字符串时出错: {e}") logger.error(f"[私聊][{self.private_name}]构建对话目标字符串时出错: {e}")
goals_str = "- 构建对话目标时出错。\n" goals_str = "- 构建对话目标时出错。\n"
# --- 知识信息字符串构建开始 ---
knowledge_info_str = "【已获取的相关知识和记忆】\n"
try:
# 检查 conversation_info 是否有 knowledge_list 并且不为空
if hasattr(conversation_info, "knowledge_list") and conversation_info.knowledge_list:
# 最多只显示最近的 5 条知识,防止 Prompt 过长
recent_knowledge = conversation_info.knowledge_list[-5:]
for i, knowledge_item in enumerate(recent_knowledge):
if isinstance(knowledge_item, dict):
query = knowledge_item.get("query", "未知查询")
knowledge = knowledge_item.get("knowledge", "无知识内容")
source = knowledge_item.get("source", "未知来源")
# 只取知识内容的前 2000 个字,避免太长
knowledge_snippet = knowledge[:2000] + "..." if len(knowledge) > 2000 else knowledge
knowledge_info_str += (
f"{i + 1}. 关于 '{query}' 的知识 (来源: {source}):\n {knowledge_snippet}\n"
)
else:
# 处理列表里不是字典的异常情况
knowledge_info_str += f"{i + 1}. 发现一条格式不正确的知识记录。\n"
if not recent_knowledge: # 如果 knowledge_list 存在但为空
knowledge_info_str += "- 暂无相关知识和记忆。\n"
else:
# 如果 conversation_info 没有 knowledge_list 属性,或者列表为空
knowledge_info_str += "- 暂无相关知识记忆。\n"
except AttributeError:
logger.warning(f"[私聊][{self.private_name}]ConversationInfo 对象可能缺少 knowledge_list 属性。")
knowledge_info_str += "- 获取知识列表时出错。\n"
except Exception as e:
logger.error(f"[私聊][{self.private_name}]构建知识信息字符串时出错: {e}")
knowledge_info_str += "- 处理知识列表时出错。\n"
# --- 知识信息字符串构建结束 ---
# 获取聊天历史记录 (chat_history_text) # 获取聊天历史记录 (chat_history_text)
chat_history_text = "" chat_history_text = ""
try: try:
@@ -349,6 +386,7 @@ class ActionPlanner:
time_since_last_bot_message_info=time_since_last_bot_message_info, time_since_last_bot_message_info=time_since_last_bot_message_info,
timeout_context=timeout_context, timeout_context=timeout_context,
chat_history_text=chat_history_text if chat_history_text.strip() else "还没有聊天记录。", chat_history_text=chat_history_text if chat_history_text.strip() else "还没有聊天记录。",
knowledge_info_str=knowledge_info_str,
) )
logger.debug(f"[私聊][{self.private_name}]发送到LLM的最终提示词:\n------\n{prompt}\n------") logger.debug(f"[私聊][{self.private_name}]发送到LLM的最终提示词:\n------\n{prompt}\n------")

View File

@@ -525,9 +525,9 @@ class Conversation:
) )
action_successful = True action_successful = True
except Exception as fetch_err: except Exception as fetch_err:
logger.error(f"[私聊][{self.private_name}]获取知识时出错: {fetch_err}") logger.error(f"[私聊][{self.private_name}]获取知识时出错: {str(fetch_err)}")
conversation_info.done_action[action_index].update( conversation_info.done_action[action_index].update(
{"status": "recall", "final_reason": f"获取知识失败: {fetch_err}"} {"status": "recall", "final_reason": f"获取知识失败: {str(fetch_err)}"}
) )
self.conversation_info.last_successful_reply_action = None # 重置状态 self.conversation_info.last_successful_reply_action = None # 重置状态

View File

@@ -50,21 +50,18 @@ class MessageStorage(ABC):
class MongoDBMessageStorage(MessageStorage): class MongoDBMessageStorage(MessageStorage):
"""MongoDB消息存储实现""" """MongoDB消息存储实现"""
def __init__(self):
self.db = db
async def get_messages_after(self, chat_id: str, message_time: float) -> List[Dict[str, Any]]: async def get_messages_after(self, chat_id: str, message_time: float) -> List[Dict[str, Any]]:
query = {"chat_id": chat_id} query = {"chat_id": chat_id}
# print(f"storage_check_message: {message_time}") # print(f"storage_check_message: {message_time}")
query["time"] = {"$gt": message_time} query["time"] = {"$gt": message_time}
return list(self.db.messages.find(query).sort("time", 1)) return list(db.messages.find(query).sort("time", 1))
async def get_messages_before(self, chat_id: str, time_point: float, limit: int = 5) -> List[Dict[str, Any]]: async def get_messages_before(self, chat_id: str, time_point: float, limit: int = 5) -> List[Dict[str, Any]]:
query = {"chat_id": chat_id, "time": {"$lt": time_point}} query = {"chat_id": chat_id, "time": {"$lt": time_point}}
messages = list(self.db.messages.find(query).sort("time", -1).limit(limit)) messages = list(db.messages.find(query).sort("time", -1).limit(limit))
# 将消息按时间正序排列 # 将消息按时间正序排列
messages.reverse() messages.reverse()
@@ -73,7 +70,7 @@ class MongoDBMessageStorage(MessageStorage):
async def has_new_messages(self, chat_id: str, after_time: float) -> bool: async def has_new_messages(self, chat_id: str, after_time: float) -> bool:
query = {"chat_id": chat_id, "time": {"$gt": after_time}} query = {"chat_id": chat_id, "time": {"$gt": after_time}}
return self.db.messages.find_one(query) is not None return db.messages.find_one(query) is not None
# # 创建一个内存消息存储实现,用于测试 # # 创建一个内存消息存储实现,用于测试

View File

@@ -68,16 +68,18 @@ class KnowledgeFetcher:
max_depth=3, max_depth=3,
fast_retrieval=False, fast_retrieval=False,
) )
knowledge = "" knowledge_text = ""
sources_text = "无记忆匹配" # 默认值
if related_memory: if related_memory:
sources = [] sources = []
for memory in related_memory: for memory in related_memory:
knowledge += memory[1] + "\n" knowledge_text += memory[1] + "\n"
sources.append(f"记忆片段{memory[0]}") sources.append(f"记忆片段{memory[0]}")
knowledge = knowledge.strip(), "".join(sources) knowledge_text = knowledge_text.strip()
sources_text = "".join(sources)
knowledge += "现在有以下**知识**可供参考:\n " knowledge_text += "\n现在有以下**知识**可供参考:\n "
knowledge += self._lpmm_get_knowledge(query) knowledge_text += self._lpmm_get_knowledge(query)
knowledge += "请记住这些**知识**,并根据**知识**回答问题。\n" knowledge_text += "\n请记住这些**知识**,并根据**知识**回答问题。\n"
return "未找到相关知识", "无记忆匹配" return knowledge_text or "未找到相关知识", sources_text or "无记忆匹配"

View File

@@ -17,6 +17,9 @@ logger = get_module_logger("reply_generator")
PROMPT_DIRECT_REPLY = """{persona_text}。现在你在参与一场QQ私聊请根据以下信息生成一条回复 PROMPT_DIRECT_REPLY = """{persona_text}。现在你在参与一场QQ私聊请根据以下信息生成一条回复
当前对话目标:{goals_str} 当前对话目标:{goals_str}
{knowledge_info_str}
最近的聊天记录: 最近的聊天记录:
{chat_history_text} {chat_history_text}
@@ -25,7 +28,7 @@ PROMPT_DIRECT_REPLY = """{persona_text}。现在你在参与一场QQ私聊
1. 符合对话目标,以""的角度发言(不要自己与自己对话!) 1. 符合对话目标,以""的角度发言(不要自己与自己对话!)
2. 符合你的性格特征和身份细节 2. 符合你的性格特征和身份细节
3. 通俗易懂自然流畅像正常聊天一样简短通常20字以内除非特殊情况 3. 通俗易懂自然流畅像正常聊天一样简短通常20字以内除非特殊情况
4. 适当利用相关知识,但不要生硬引用 4. 可以适当利用相关知识,但不要生硬引用
5. 自然、得体,结合聊天记录逻辑合理,且没有重复表达同质内容 5. 自然、得体,结合聊天记录逻辑合理,且没有重复表达同质内容
请注意把握聊天内容,不要回复的太有条理,可以有个性。请分清""和对方说的话,不要把""说的话当做对方说的话,这是你自己说的话。 请注意把握聊天内容,不要回复的太有条理,可以有个性。请分清""和对方说的话,不要把""说的话当做对方说的话,这是你自己说的话。
@@ -39,6 +42,9 @@ PROMPT_DIRECT_REPLY = """{persona_text}。现在你在参与一场QQ私聊
PROMPT_SEND_NEW_MESSAGE = """{persona_text}。现在你在参与一场QQ私聊**刚刚你已经发送了一条或多条消息**,现在请根据以下信息再发一条新消息: PROMPT_SEND_NEW_MESSAGE = """{persona_text}。现在你在参与一场QQ私聊**刚刚你已经发送了一条或多条消息**,现在请根据以下信息再发一条新消息:
当前对话目标:{goals_str} 当前对话目标:{goals_str}
{knowledge_info_str}
最近的聊天记录: 最近的聊天记录:
{chat_history_text} {chat_history_text}
@@ -47,7 +53,7 @@ PROMPT_SEND_NEW_MESSAGE = """{persona_text}。现在你在参与一场QQ私聊
1. 符合对话目标,以""的角度发言(不要自己与自己对话!) 1. 符合对话目标,以""的角度发言(不要自己与自己对话!)
2. 符合你的性格特征和身份细节 2. 符合你的性格特征和身份细节
3. 通俗易懂自然流畅像正常聊天一样简短通常20字以内除非特殊情况 3. 通俗易懂自然流畅像正常聊天一样简短通常20字以内除非特殊情况
4. 适当利用相关知识,但不要生硬引用 4. 可以适当利用相关知识,但不要生硬引用
5. 跟之前你发的消息自然的衔接,逻辑合理,且没有重复表达同质内容或部分重叠内容 5. 跟之前你发的消息自然的衔接,逻辑合理,且没有重复表达同质内容或部分重叠内容
请注意把握聊天内容,不用太有条理,可以有个性。请分清""和对方说的话,不要把""说的话当做对方说的话,这是你自己说的话。 请注意把握聊天内容,不用太有条理,可以有个性。请分清""和对方说的话,不要把""说的话当做对方说的话,这是你自己说的话。
@@ -131,6 +137,38 @@ class ReplyGenerator:
else: else:
goals_str = "- 目前没有明确对话目标\n" # 简化无目标情况 goals_str = "- 目前没有明确对话目标\n" # 简化无目标情况
# --- 新增:构建知识信息字符串 ---
knowledge_info_str = "【供参考的相关知识和记忆】\n" # 稍微改下标题,表明是供参考
try:
# 检查 conversation_info 是否有 knowledge_list 并且不为空
if hasattr(conversation_info, "knowledge_list") and conversation_info.knowledge_list:
# 最多只显示最近的 5 条知识
recent_knowledge = conversation_info.knowledge_list[-5:]
for i, knowledge_item in enumerate(recent_knowledge):
if isinstance(knowledge_item, dict):
query = knowledge_item.get("query", "未知查询")
knowledge = knowledge_item.get("knowledge", "无知识内容")
source = knowledge_item.get("source", "未知来源")
# 只取知识内容的前 2000 个字
knowledge_snippet = knowledge[:2000] + "..." if len(knowledge) > 2000 else knowledge
knowledge_info_str += (
f"{i + 1}. 关于 '{query}' (来源: {source}): {knowledge_snippet}\n" # 格式微调,更简洁
)
else:
knowledge_info_str += f"{i + 1}. 发现一条格式不正确的知识记录。\n"
if not recent_knowledge:
knowledge_info_str += "- 暂无。\n" # 更简洁的提示
else:
knowledge_info_str += "- 暂无。\n"
except AttributeError:
logger.warning(f"[私聊][{self.private_name}]ConversationInfo 对象可能缺少 knowledge_list 属性。")
knowledge_info_str += "- 获取知识列表时出错。\n"
except Exception as e:
logger.error(f"[私聊][{self.private_name}]构建知识信息字符串时出错: {e}")
knowledge_info_str += "- 处理知识列表时出错。\n"
# 获取聊天历史记录 (chat_history_text) # 获取聊天历史记录 (chat_history_text)
chat_history_text = observation_info.chat_history_str chat_history_text = observation_info.chat_history_str
if observation_info.new_messages_count > 0 and observation_info.unprocessed_messages: if observation_info.new_messages_count > 0 and observation_info.unprocessed_messages:
@@ -162,7 +200,10 @@ class ReplyGenerator:
# --- 格式化最终的 Prompt --- # --- 格式化最终的 Prompt ---
prompt = prompt_template.format( prompt = prompt_template.format(
persona_text=persona_text, goals_str=goals_str, chat_history_text=chat_history_text persona_text=persona_text,
goals_str=goals_str,
chat_history_text=chat_history_text,
knowledge_info_str=knowledge_info_str,
) )
# --- 调用 LLM 生成 --- # --- 调用 LLM 生成 ---

View File

@@ -99,15 +99,20 @@ class ChatBot:
template_group_name = None template_group_name = None
async def preprocess(): async def preprocess():
logger.trace("开始预处理消息...")
# 如果在私聊中 # 如果在私聊中
if groupinfo is None: if groupinfo is None:
logger.trace("检测到私聊消息")
# 是否在配置信息中开启私聊模式 # 是否在配置信息中开启私聊模式
if global_config.enable_friend_chat: if global_config.enable_friend_chat:
logger.trace("私聊模式已启用")
# 是否进入PFC # 是否进入PFC
if global_config.enable_pfc_chatting: if global_config.enable_pfc_chatting:
logger.trace("进入PFC私聊处理流程")
userinfo = message.message_info.user_info userinfo = message.message_info.user_info
messageinfo = message.message_info messageinfo = message.message_info
# 创建聊天流 # 创建聊天流
logger.trace(f"{userinfo.user_id}创建/获取聊天流")
chat = await chat_manager.get_or_create_stream( chat = await chat_manager.get_or_create_stream(
platform=messageinfo.platform, platform=messageinfo.platform,
user_info=userinfo, user_info=userinfo,
@@ -118,9 +123,11 @@ class ChatBot:
await self._create_pfc_chat(message) await self._create_pfc_chat(message)
# 禁止PFC进入普通的心流消息处理逻辑 # 禁止PFC进入普通的心流消息处理逻辑
else: else:
logger.trace("进入普通心流私聊处理")
await self.heartflow_processor.process_message(message_data) await self.heartflow_processor.process_message(message_data)
# 群聊默认进入心流消息处理逻辑 # 群聊默认进入心流消息处理逻辑
else: else:
logger.trace(f"检测到群聊消息群ID: {groupinfo.group_id}")
await self.heartflow_processor.process_message(message_data) await self.heartflow_processor.process_message(message_data)
if template_group_name: if template_group_name:

View File

@@ -159,16 +159,16 @@ class MessageManager:
logger.warning("Processor task already running.") logger.warning("Processor task already running.")
return return
self._processor_task = asyncio.create_task(self._start_processor_loop()) self._processor_task = asyncio.create_task(self._start_processor_loop())
logger.info("MessageManager processor task started.") logger.debug("MessageManager processor task started.")
def stop(self): def stop(self):
"""停止后台处理器任务。""" """停止后台处理器任务。"""
self._running = False self._running = False
if hasattr(self, "_processor_task") and not self._processor_task.done(): if hasattr(self, "_processor_task") and not self._processor_task.done():
self._processor_task.cancel() self._processor_task.cancel()
logger.info("MessageManager processor task stopping.") logger.debug("MessageManager processor task stopping.")
else: else:
logger.info("MessageManager processor task not running or already stopped.") logger.debug("MessageManager processor task not running or already stopped.")
async def get_container(self, chat_id: str) -> MessageContainer: async def get_container(self, chat_id: str) -> MessageContainer:
"""获取或创建聊天流的消息容器 (异步,使用锁)""" """获取或创建聊天流的消息容器 (异步,使用锁)"""

View File

@@ -732,6 +732,9 @@ def translate_timestamp_to_human_readable(timestamp: float, mode: str = "normal"
return f"{int(diff / 86400)}天前:\n" return f"{int(diff / 86400)}天前:\n"
else: else:
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp)) + ":\n" return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp)) + ":\n"
elif mode == "lite":
# 只返回时分秒格式,喵~
return time.strftime("%H:%M:%S", time.localtime(timestamp))
return None return None

View File

@@ -5,6 +5,7 @@ import hashlib
from typing import Optional from typing import Optional
from PIL import Image from PIL import Image
import io import io
import numpy as np
from ...common.database import db from ...common.database import db
@@ -231,14 +232,16 @@ class ImageManager:
return "[图片]" return "[图片]"
@staticmethod @staticmethod
def transform_gif(gif_base64: str) -> str: def transform_gif(gif_base64: str, similarity_threshold: float = 1000.0, max_frames: int = 15) -> Optional[str]:
"""将GIF转换为水平拼接的静态图像 """将GIF转换为水平拼接的静态图像, 跳过相似的帧
Args: Args:
gif_base64: GIF的base64编码字符串 gif_base64: GIF的base64编码字符串
similarity_threshold: 判定帧相似的阈值 (MSE)越小表示要求差异越大才算不同帧默认1000.0
max_frames: 最大抽取的帧数默认15
Returns: Returns:
str: 拼接后的JPG图像的base64编码字符串 Optional[str]: 拼接后的JPG图像的base64编码字符串, 或者在失败时返回None
""" """
try: try:
# 解码base64 # 解码base64
@@ -246,41 +249,88 @@ class ImageManager:
gif = Image.open(io.BytesIO(gif_data)) gif = Image.open(io.BytesIO(gif_data))
# 收集所有帧 # 收集所有帧
frames = [] all_frames = []
try: try:
while True: while True:
gif.seek(len(frames)) gif.seek(len(all_frames))
# 确保是RGB格式方便比较
frame = gif.convert("RGB") frame = gif.convert("RGB")
frames.append(frame.copy()) all_frames.append(frame.copy())
except EOFError: except EOFError:
pass pass # 读完啦
if not frames: if not all_frames:
raise ValueError("No frames found in GIF") logger.warning("GIF中没有找到任何帧")
return None # 空的GIF直接返回None
# 计算需要抽取的帧的索引 # --- 新的帧选择逻辑 ---
total_frames = len(frames) selected_frames = []
if total_frames <= 15: last_selected_frame_np = None
selected_frames = frames
else:
# 均匀抽取10帧
indices = [int(i * (total_frames - 1) / 14) for i in range(15)]
selected_frames = [frames[i] for i in indices]
# 获取单帧的尺寸 for i, current_frame in enumerate(all_frames):
current_frame_np = np.array(current_frame)
# 第一帧总是要选的
if i == 0:
selected_frames.append(current_frame)
last_selected_frame_np = current_frame_np
continue
# 计算和上一张选中帧的差异(均方误差 MSE
if last_selected_frame_np is not None:
mse = np.mean((current_frame_np - last_selected_frame_np) ** 2)
# logger.trace(f"帧 {i} 与上一选中帧的 MSE: {mse}") # 可以取消注释来看差异值
# 如果差异够大,就选它!
if mse > similarity_threshold:
selected_frames.append(current_frame)
last_selected_frame_np = current_frame_np
# 检查是不是选够了
if len(selected_frames) >= max_frames:
# logger.debug(f"已选够 {max_frames} 帧,停止选择。")
break
# 如果差异不大就跳过这一帧啦
# --- 帧选择逻辑结束 ---
# 如果选择后连一帧都没有比如GIF只有一帧且后续处理失败或者原始GIF就没帧也返回None
if not selected_frames:
logger.warning("处理后没有选中任何帧")
return None
# logger.debug(f"总帧数: {len(all_frames)}, 选中帧数: {len(selected_frames)}")
# 获取选中的第一帧的尺寸(假设所有帧尺寸一致)
frame_width, frame_height = selected_frames[0].size frame_width, frame_height = selected_frames[0].size
# 计算目标尺寸,保持宽高比 # 计算目标尺寸,保持宽高比
target_height = 200 # 固定高度 target_height = 200 # 固定高度
# 防止除以零
if frame_height == 0:
logger.error("帧高度为0无法计算缩放尺寸")
return None
target_width = int((target_height / frame_height) * frame_width) target_width = int((target_height / frame_height) * frame_width)
# 宽度也不能是0
if target_width == 0:
logger.warning(f"计算出的目标宽度为0 (原始尺寸 {frame_width}x{frame_height})调整为1")
target_width = 1
# 调整所有帧的大小 # 调整所有选中帧的大小
resized_frames = [ resized_frames = [
frame.resize((target_width, target_height), Image.Resampling.LANCZOS) for frame in selected_frames frame.resize((target_width, target_height), Image.Resampling.LANCZOS) for frame in selected_frames
] ]
# 创建拼接图像 # 创建拼接图像
total_width = target_width * len(resized_frames) total_width = target_width * len(resized_frames)
# 防止总宽度为0
if total_width == 0 and len(resized_frames) > 0:
logger.warning("计算出的总宽度为0但有选中帧可能目标宽度太小")
# 至少给点宽度吧
total_width = len(resized_frames)
elif total_width == 0:
logger.error("计算出的总宽度为0且无选中帧")
return None
combined_image = Image.new("RGB", (total_width, target_height)) combined_image = Image.new("RGB", (total_width, target_height))
# 水平拼接图像 # 水平拼接图像
@@ -289,14 +339,17 @@ class ImageManager:
# 转换为base64 # 转换为base64
buffer = io.BytesIO() buffer = io.BytesIO()
combined_image.save(buffer, format="JPEG", quality=85) combined_image.save(buffer, format="JPEG", quality=85) # 保存为JPEG
result_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8") result_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
return result_base64 return result_base64
except MemoryError:
logger.error("GIF转换失败: 内存不足可能是GIF太大或帧数太多")
return None # 内存不够啦
except Exception as e: except Exception as e:
logger.error(f"GIF转换失败: {str(e)}") logger.error(f"GIF转换失败: {str(e)}", exc_info=True) # 记录详细错误信息
return None return None # 其他错误也返回None
# 创建全局单例 # 创建全局单例

View File

@@ -34,9 +34,12 @@ MAX_EMOJI_FOR_PROMPT = 20 # 最大允许的表情包描述数量于图片替换
class MaiEmoji: class MaiEmoji:
"""定义一个表情包""" """定义一个表情包"""
def __init__(self, filename: str, path: str): def __init__(self, full_path: str):
self.path = path # 存储目录路径 if not full_path:
self.filename = filename raise ValueError("full_path cannot be empty")
self.full_path = full_path # 文件的完整路径 (包括文件名)
self.path = os.path.dirname(full_path) # 文件所在的目录路径
self.filename = os.path.basename(full_path) # 文件名
self.embedding = [] self.embedding = []
self.hash = "" # 初始为空,在创建实例时会计算 self.hash = "" # 初始为空,在创建实例时会计算
self.description = "" self.description = ""
@@ -48,35 +51,58 @@ class MaiEmoji:
self.format = "" self.format = ""
async def initialize_hash_format(self): async def initialize_hash_format(self):
"""从文件创建表情包实例 """从文件创建表情包实例, 计算哈希值和格式"""
image_base64 = None
参数: image_bytes = None
file_path: 文件的完整路径
返回:
MaiEmoji: 创建的表情包实例如果失败则返回None
"""
try: try:
file_path = os.path.join(self.path, self.filename) # 使用 full_path 检查文件是否存在
if not os.path.exists(file_path): if not os.path.exists(self.full_path):
logger.error(f"[错误] 表情包文件不存在: {file_path}") logger.error(f"[初始化错误] 表情包文件不存在: {self.full_path}")
self.is_deleted = True
return None return None
image_base64 = image_path_to_base64(file_path) # 使用 full_path 读取文件
logger.debug(f"[初始化] 正在读取文件: {self.full_path}")
image_base64 = image_path_to_base64(self.full_path)
if image_base64 is None: if image_base64 is None:
logger.error(f"[错误] 无法读取图片: {file_path}") logger.error(f"[初始化错误] 无法读取或转换Base64: {self.full_path}")
self.is_deleted = True
return None return None
logger.debug(f"[初始化] 文件读取成功 (Base64预览: {image_base64[:50]}...)")
# 计算哈希值 # 计算哈希值
logger.debug(f"[初始化] 正在解码Base64并计算哈希: {self.filename}")
image_bytes = base64.b64decode(image_base64) image_bytes = base64.b64decode(image_base64)
self.hash = hashlib.md5(image_bytes).hexdigest() self.hash = hashlib.md5(image_bytes).hexdigest()
logger.debug(f"[初始化] 哈希计算成功: {self.hash}")
# 获取图片格式 # 获取图片格式
self.format = Image.open(io.BytesIO(image_bytes)).format.lower() logger.debug(f"[初始化] 正在使用Pillow获取格式: {self.filename}")
try:
with Image.open(io.BytesIO(image_bytes)) as img:
self.format = img.format.lower()
logger.debug(f"[初始化] 格式获取成功: {self.format}")
except Exception as pil_error:
logger.error(f"[初始化错误] Pillow无法处理图片 ({self.filename}): {pil_error}")
logger.error(traceback.format_exc())
self.is_deleted = True
return None
# 如果所有步骤成功,返回 True
return True
except FileNotFoundError:
logger.error(f"[初始化错误] 文件在处理过程中丢失: {self.full_path}")
self.is_deleted = True
return None
except base64.binascii.Error as b64_error:
logger.error(f"[初始化错误] Base64解码失败 ({self.filename}): {b64_error}")
self.is_deleted = True
return None
except Exception as e: except Exception as e:
logger.error(f"[错误] 初始化表情包失败: {str(e)}") logger.error(f"[初始化错误] 初始化表情包时发生未预期错误 ({self.filename}): {str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
self.is_deleted = True
return None return None
async def register_to_db(self): async def register_to_db(self):
@@ -87,61 +113,72 @@ class MaiEmoji:
""" """
try: try:
# 确保目标目录存在 # 确保目标目录存在
os.makedirs(EMOJI_REGISTED_DIR, exist_ok=True)
# 源路径是当前实例的完整路径 # 源路径是当前实例的完整路径 self.full_path
source_path = os.path.join(self.path, self.filename) source_full_path = self.full_path
# 目标路径 # 目标完整路径
destination_path = os.path.join(EMOJI_REGISTED_DIR, self.filename) destination_full_path = os.path.join(EMOJI_REGISTED_DIR, self.filename)
# 检查源文件是否存在 # 检查源文件是否存在
if not os.path.exists(source_path): if not os.path.exists(source_full_path):
logger.error(f"[错误] 源文件不存在: {source_path}") logger.error(f"[错误] 源文件不存在: {source_full_path}")
return False return False
# --- 文件移动 --- # --- 文件移动 ---
try: try:
# 如果目标文件已存在,先删除 (确保移动成功) # 如果目标文件已存在,先删除 (确保移动成功)
if os.path.exists(destination_path): if os.path.exists(destination_full_path):
os.remove(destination_path) os.remove(destination_full_path)
os.rename(source_path, destination_path) os.rename(source_full_path, destination_full_path)
logger.info(f"[移动] 文件从 {source_path} 移动到 {destination_path}") logger.debug(f"[移动] 文件从 {source_full_path} 移动到 {destination_full_path}")
# 更新实例的路径属性为新目录 # 更新实例的路径属性为新路径
self.full_path = destination_full_path
self.path = EMOJI_REGISTED_DIR self.path = EMOJI_REGISTED_DIR
# self.filename 保持不变
except Exception as move_error: except Exception as move_error:
logger.error(f"[错误] 移动文件失败: {str(move_error)}") logger.error(f"[错误] 移动文件失败: {str(move_error)}")
return False # 文件移动失败,不继续 # 如果移动失败,尝试将实例状态恢复?暂时不处理,仅返回失败
return False
# --- 数据库操作 --- # --- 数据库操作 ---
try: try:
# 准备数据库记录 for emoji collection # 准备数据库记录 for emoji collection
emoji_record = { emoji_record = {
"filename": self.filename, "filename": self.filename,
"path": os.path.join(self.path, self.filename), # 使用更新后的路径 "path": self.path, # 存储目录路径
"full_path": self.full_path, # 存储完整文件路径
"embedding": self.embedding, "embedding": self.embedding,
"description": self.description, "description": self.description,
"emotion": self.emotion, # 添加情感标签字段 "emotion": self.emotion,
"hash": self.hash, "hash": self.hash,
"format": self.format, "format": self.format,
"timestamp": int(self.register_time), # 使用实例的注册时间 "timestamp": int(self.register_time),
"usage_count": self.usage_count, "usage_count": self.usage_count,
"last_used_time": self.last_used_time, "last_used_time": self.last_used_time,
} }
# 使用upsert确保记录存在或被更新 # 使用upsert确保记录存在或被更新
db["emoji"].update_one({"hash": self.hash}, {"$set": emoji_record}, upsert=True) db["emoji"].update_one({"hash": self.hash}, {"$set": emoji_record}, upsert=True)
logger.success(f"[注册] 表情包信息保存到数据库: {self.description}")
logger.success(f"[注册] 表情包信息保存到数据库: {self.filename} ({self.emotion})")
return True return True
except Exception as db_error: except Exception as db_error:
logger.error(f"[错误] 保存数据库失败: {str(db_error)}") logger.error(f"[错误] 保存数据库失败 ({self.filename}): {str(db_error)}")
# 考虑是否需要将文件移回?为了简化,暂时只记录错误 # 数据库保存失败,是否需要将文件移回?为了简化,暂时只记录错误
# 可以考虑在这里尝试删除已移动的文件,避免残留
try:
if os.path.exists(self.full_path): # full_path 此时是目标路径
os.remove(self.full_path)
logger.warning(f"[回滚] 已删除移动失败后残留的文件: {self.full_path}")
except Exception as remove_error:
logger.error(f"[错误] 回滚删除文件失败: {remove_error}")
return False return False
except Exception as e: except Exception as e:
logger.error(f"[错误] 注册表情包失败: {str(e)}") logger.error(f"[错误] 注册表情包失败 ({self.filename}): {str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return False return False
@@ -155,30 +192,36 @@ class MaiEmoji:
""" """
try: try:
# 1. 删除文件 # 1. 删除文件
if os.path.exists(os.path.join(self.path, self.filename)): file_to_delete = self.full_path
if os.path.exists(file_to_delete):
try: try:
os.remove(os.path.join(self.path, self.filename)) os.remove(file_to_delete)
logger.info(f"[删除] 文件: {os.path.join(self.path, self.filename)}") logger.debug(f"[删除] 文件: {file_to_delete}")
except Exception as e: except Exception as e:
logger.error(f"[错误] 删除文件失败 {os.path.join(self.path, self.filename)}: {str(e)}") logger.error(f"[错误] 删除文件失败 {file_to_delete}: {str(e)}")
# 继续执行,即使文件删除失败尝试删除数据库记录 # 文件删除失败,但仍然尝试删除数据库记录
# 2. 删除数据库记录 # 2. 删除数据库记录
result = db.emoji.delete_one({"hash": self.hash}) result = db.emoji.delete_one({"hash": self.hash})
deleted_in_db = result.deleted_count > 0 deleted_in_db = result.deleted_count > 0
if deleted_in_db: if deleted_in_db:
logger.success(f"[删除] 成功删除表情包记录: {self.description}") logger.info(f"[删除] 表情包数据库记录 {self.filename} (Hash: {self.hash})")
# 3. 标记对象已被删除 # 3. 标记对象已被删除
self.is_deleted = True self.is_deleted = True
return True return True
else: else:
logger.error(f"[错误] 删除表情包记录失败: {self.hash}") # 如果数据库记录删除失败,但文件可能已删除,记录一个警告
if not os.path.exists(file_to_delete):
logger.warning(
f"[警告] 表情包文件 {file_to_delete} 已删除,但数据库记录删除失败 (Hash: {self.hash})"
)
else:
logger.error(f"[错误] 删除表情包数据库记录失败: {self.hash}")
return False return False
except Exception as e: except Exception as e:
logger.error(f"[错误] 删除表情包失败: {str(e)}") logger.error(f"[错误] 删除表情包失败 ({self.filename}): {str(e)}")
return False return False
@@ -195,7 +238,7 @@ class EmojiManager:
self._scan_task = None self._scan_task = None
self.vlm = LLMRequest(model=global_config.vlm, temperature=0.3, max_tokens=1000, request_type="emoji") self.vlm = LLMRequest(model=global_config.vlm, temperature=0.3, max_tokens=1000, request_type="emoji")
self.llm_emotion_judge = LLMRequest( self.llm_emotion_judge = LLMRequest(
model=global_config.llm_emotion_judge, max_tokens=600, temperature=0.8, request_type="emoji" model=global_config.llm_normal, max_tokens=600, request_type="emoji"
) # 更高的温度更少的token后续可以根据情绪来调整温度 ) # 更高的温度更少的token后续可以根据情绪来调整温度
self.emoji_num = 0 self.emoji_num = 0
@@ -208,6 +251,7 @@ class EmojiManager:
def _ensure_emoji_dir(self): def _ensure_emoji_dir(self):
"""确保表情存储目录存在""" """确保表情存储目录存在"""
os.makedirs(EMOJI_DIR, exist_ok=True) os.makedirs(EMOJI_DIR, exist_ok=True)
os.makedirs(EMOJI_REGISTED_DIR, exist_ok=True)
def initialize(self): def initialize(self):
"""初始化数据库连接和表情目录""" """初始化数据库连接和表情目录"""
@@ -264,59 +308,71 @@ class EmojiManager:
Args: Args:
text_emotion: 输入的情感描述文本 text_emotion: 输入的情感描述文本
Returns: Returns:
Optional[Tuple[str, str]]: (表情包文件路径, 表情包描述)如果没有找到则返回None Optional[Tuple[str, str]]: (表情包完整文件路径, 表情包描述)如果没有找到则返回None
""" """
try: try:
self._ensure_db() self._ensure_db()
time_start = time.time() _time_start = time.time()
# 获取所有表情包 # 获取所有表情包 (从内存缓存中获取)
all_emojis = self.emoji_objects all_emojis = self.emoji_objects
if not all_emojis: if not all_emojis:
logger.warning("数据库中没有任何表情包") logger.warning("内存中没有任何表情包对象")
# 可以考虑再查一次数据库?或者依赖定期任务更新
return None return None
# 计算每个表情包与输入文本的最大情感相似度 # 计算每个表情包与输入文本的最大情感相似度
emoji_similarities = [] emoji_similarities = []
for emoji in all_emojis: for emoji in all_emojis:
# 跳过已标记为删除的对象
if emoji.is_deleted:
continue
emotions = emoji.emotion emotions = emoji.emotion
if not emotions: if not emotions:
continue continue
# 计算与每个emotion标签的相似度取最大值 # 计算与每个emotion标签的相似度取最大值
max_similarity = 0 max_similarity = 0
best_matching_emotion = "" # 记录最匹配的 emotion 喵~
for emotion in emotions: for emotion in emotions:
# 使用编辑距离计算相似度 # 使用编辑距离计算相似度
distance = self._levenshtein_distance(text_emotion, emotion) distance = self._levenshtein_distance(text_emotion, emotion)
max_len = max(len(text_emotion), len(emotion)) max_len = max(len(text_emotion), len(emotion))
similarity = 1 - (distance / max_len if max_len > 0 else 0) similarity = 1 - (distance / max_len if max_len > 0 else 0)
max_similarity = max(max_similarity, similarity) if similarity > max_similarity: # 如果找到更相似的喵~
max_similarity = similarity
best_matching_emotion = emotion # 就记下这个 emotion 喵~
emoji_similarities.append((emoji, max_similarity)) if best_matching_emotion: # 确保有匹配的情感才添加喵~
emoji_similarities.append((emoji, max_similarity, best_matching_emotion)) # 把 emotion 也存起来喵~
# 按相似度降序排序 # 按相似度降序排序
emoji_similarities.sort(key=lambda x: x[1], reverse=True) emoji_similarities.sort(key=lambda x: x[1], reverse=True)
# 获取前5个最相似的表情包 # 获取前10个最相似的表情包
top_5_emojis = emoji_similarities[:10] if len(emoji_similarities) > 10 else emoji_similarities top_emojis = (
emoji_similarities[:10] if len(emoji_similarities) > 10 else emoji_similarities
) # 改个名字,更清晰喵~
if not top_5_emojis: if not top_emojis:
logger.warning("未找到匹配的表情包") logger.warning("未找到匹配的表情包")
return None return None
# 从前5个中随机选择一个 # 从前个中随机选择一个
selected_emoji, similarity = random.choice(top_5_emojis) selected_emoji, similarity, matched_emotion = random.choice(top_emojis) # 把匹配的 emotion 也拿出来喵~
# 更新使用次数 # 更新使用次数
self.record_usage(selected_emoji.hash) self.record_usage(selected_emoji.hash)
time_end = time.time() _time_end = time.time()
logger.info( logger.info( # 使用匹配到的 emotion 记录日志喵~
f"找到[{text_emotion}]表情包,用时:{time_end - time_start:.2f}秒: {selected_emoji.description} (相似度: {similarity:.4f})" f"[{text_emotion}]找到表情包: {matched_emotion} ({selected_emoji.filename}), Similarity: {similarity:.4f}"
) )
return selected_emoji.path, f"[ {selected_emoji.description} ]" # 返回完整文件路径和描述
return selected_emoji.full_path, f"[ {selected_emoji.description} ]"
except Exception as e: except Exception as e:
logger.error(f"[错误] 获取表情包失败: {str(e)}") logger.error(f"[错误] 获取表情包失败: {str(e)}")
@@ -364,40 +420,50 @@ class EmojiManager:
self.emoji_num = total_count self.emoji_num = total_count
removed_count = 0 removed_count = 0
# 使用列表复制进行遍历,因为我们会在遍历过程中修改列表 # 使用列表复制进行遍历,因为我们会在遍历过程中修改列表
for emoji in self.emoji_objects[:]: objects_to_remove = []
for emoji in self.emoji_objects:
try: try:
# 跳过已经标记为删除的,避免重复处理
if emoji.is_deleted:
objects_to_remove.append(emoji) # 收集起来一次性移除
continue
# 检查文件是否存在 # 检查文件是否存在
if not os.path.exists(emoji.path): if not os.path.exists(emoji.full_path):
logger.warning(f"[检查] 表情包文件已被删除: {emoji.path}") logger.warning(f"[检查] 表情包文件丢失: {emoji.full_path}")
# 执行表情包对象的删除方法 # 执行表情包对象的删除方法
await emoji.delete() await emoji.delete() # delete 方法现在会标记 is_deleted
# 从列表中移除该对象 objects_to_remove.append(emoji) # 标记删除后,也收集起来移除
self.emoji_objects.remove(emoji)
# 更新计数 # 更新计数
self.emoji_num -= 1 self.emoji_num -= 1
removed_count += 1 removed_count += 1
continue continue
if emoji.description == None: # 检查描述是否为空 (如果为空也视为无效)
logger.warning(f"[检查] 表情包文件已被删除: {emoji.path}") if not emoji.description:
# 执行表情包对象的删除方法 logger.warning(f"[检查] 表情包描述为空,视为无效: {emoji.filename}")
await emoji.delete() await emoji.delete()
# 从列表中移除该对象 objects_to_remove.append(emoji)
self.emoji_objects.remove(emoji)
# 更新计数
self.emoji_num -= 1 self.emoji_num -= 1
removed_count += 1 removed_count += 1
continue continue
except Exception as item_error: except Exception as item_error:
logger.error(f"[错误] 处理表情包记录时出错: {str(item_error)}") logger.error(f"[错误] 处理表情包记录时出错 ({emoji.filename}): {str(item_error)}")
# 即使出错,也尝试继续检查下一个
continue continue
# 从 self.emoji_objects 中移除标记的对象
if objects_to_remove:
self.emoji_objects = [e for e in self.emoji_objects if e not in objects_to_remove]
# 清理 EMOJI_REGISTED_DIR 目录中未被追踪的文件
await self.clean_unused_emojis(EMOJI_REGISTED_DIR, self.emoji_objects) await self.clean_unused_emojis(EMOJI_REGISTED_DIR, self.emoji_objects)
# 输出清理结果 # 输出清理结果
if removed_count > 0: if removed_count > 0:
logger.success(f"[清理] 已清理 {removed_count} 个失效的表情包记录") logger.success(f"[清理] 已清理 {removed_count} 个失效/文件丢失的表情包记录")
logger.info(f"[统计] 清理前: {total_count} | 清理后: {len(self.emoji_objects)}") logger.info(f"[统计] 清理前记录数: {total_count} | 清理后有效记录数: {len(self.emoji_objects)}")
else: else:
logger.info(f"[检查] 已检查 {total_count} 个表情包记录,全部完好") logger.info(f"[检查] 已检查 {total_count} 个表情包记录,全部完好")
@@ -460,45 +526,72 @@ class EmojiManager:
await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60) await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60)
async def get_all_emoji_from_db(self): async def get_all_emoji_from_db(self):
"""获取所有表情包并初始化为MaiEmoji类对象 """获取所有表情包并初始化为MaiEmoji类对象,更新 self.emoji_objects"""
参数:
hash: 可选,如果提供则只返回指定哈希值的表情包
返回:
list[MaiEmoji]: 表情包对象列表
"""
try: try:
self._ensure_db() self._ensure_db()
logger.info("[数据库] 开始加载所有表情包记录...")
# 获取所有表情包
all_emoji_data = list(db.emoji.find()) all_emoji_data = list(db.emoji.find())
# 将数据库记录转换为MaiEmoji对象
emoji_objects = [] emoji_objects = []
load_errors = 0
for emoji_data in all_emoji_data: for emoji_data in all_emoji_data:
emoji = MaiEmoji( full_path = emoji_data.get("full_path")
filename=emoji_data.get("filename", ""), if not full_path:
path=emoji_data.get("path", ""), logger.warning(f"[加载错误] 数据库记录缺少 'full_path' 字段: {emoji_data.get('_id')}")
) load_errors += 1
continue # 跳过缺少 full_path 的记录
# 设置额外属性 try:
emoji.hash = emoji_data.get("hash", "") # 使用 full_path 初始化 MaiEmoji 对象
emoji.usage_count = emoji_data.get("usage_count", 0) emoji = MaiEmoji(full_path=full_path)
emoji.last_used_time = emoji_data.get("last_used_time", emoji_data.get("timestamp", time.time()))
emoji.register_time = emoji_data.get("timestamp", time.time())
emoji.description = emoji_data.get("description", "")
emoji.emotion = emoji_data.get("emotion", []) # 添加情感标签的加载
emoji_objects.append(emoji)
# 存储到EmojiManager中 # 设置从数据库加载的属性
emoji.hash = emoji_data.get("hash", "")
# 如果 hash 为空,也跳过?取决于业务逻辑
if not emoji.hash:
logger.warning(f"[加载错误] 数据库记录缺少 'hash' 字段: {full_path}")
load_errors += 1
continue
emoji.description = emoji_data.get("description", "")
emoji.emotion = emoji_data.get("emotion", [])
emoji.usage_count = emoji_data.get("usage_count", 0)
# 优先使用 last_used_time否则用 timestamp最后用当前时间
last_used = emoji_data.get("last_used_time")
timestamp = emoji_data.get("timestamp")
emoji.last_used_time = (
last_used if last_used is not None else (timestamp if timestamp is not None else time.time())
)
emoji.register_time = timestamp if timestamp is not None else time.time()
emoji.format = emoji_data.get("format", "") # 加载格式
# 不需要再手动设置 path 和 filename__init__ 会自动处理
emoji_objects.append(emoji)
except ValueError as ve: # 捕获 __init__ 可能的错误
logger.error(f"[加载错误] 初始化 MaiEmoji 失败 ({full_path}): {ve}")
load_errors += 1
except Exception as e:
logger.error(f"[加载错误] 处理数据库记录时出错 ({full_path}): {str(e)}")
load_errors += 1
# 更新内存中的列表和数量
self.emoji_objects = emoji_objects self.emoji_objects = emoji_objects
self.emoji_num = len(emoji_objects)
logger.success(f"[数据库] 加载完成: 共加载 {self.emoji_num} 个表情包记录。")
if load_errors > 0:
logger.warning(f"[数据库] 加载过程中出现 {load_errors} 个错误。")
except Exception as e: except Exception as e:
logger.error(f"[错误] 获取所有表情包对象失败: {str(e)}") logger.error(f"[错误] 从数据库加载所有表情包对象失败: {str(e)}")
self.emoji_objects = [] # 加载失败则清空列表
self.emoji_num = 0
async def get_emoji_from_db(self, hash=None): async def get_emoji_from_db(self, hash=None):
"""获取所有表情包并初始化为MaiEmoji类对象 """获取指定哈希值的表情包并初始化为MaiEmoji类对象列表 (主要用于调试或特定查找)
参数: 参数:
hash: 可选,如果提供则只返回指定哈希值的表情包 hash: 可选,如果提供则只返回指定哈希值的表情包
@@ -509,50 +602,73 @@ class EmojiManager:
try: try:
self._ensure_db() self._ensure_db()
# 准备查询条件
query = {} query = {}
if hash: if hash:
query = {"hash": hash} query = {"hash": hash}
else:
# 获取所有表情包 logger.warning(
all_emoji_data = list(db.emoji.find(query)) "[查询] 未提供 hash将尝试加载所有表情包建议使用 get_all_emoji_from_db 更新管理器状态。"
# 将数据库记录转换为MaiEmoji对象
emoji_objects = []
for emoji_data in all_emoji_data:
emoji = MaiEmoji(
filename=emoji_data.get("filename", ""),
path=emoji_data.get("path", ""),
) )
# 设置额外属性 emoji_data_list = list(db.emoji.find(query))
emoji.usage_count = emoji_data.get("usage_count", 0) emoji_objects = []
emoji.last_used_time = emoji_data.get("last_used_time", emoji_data.get("timestamp", time.time())) load_errors = 0
emoji.register_time = emoji_data.get("timestamp", time.time())
emoji.description = emoji_data.get("description", "")
emoji.emotion = emoji_data.get("emotion", []) # 添加情感标签的加载
emoji_objects.append(emoji) for emoji_data in emoji_data_list:
full_path = emoji_data.get("full_path")
if not full_path:
logger.warning(f"[加载错误] 数据库记录缺少 'full_path' 字段: {emoji_data.get('_id')}")
load_errors += 1
continue
# 存储到EmojiManager中 try:
self.emoji_objects = emoji_objects emoji = MaiEmoji(full_path=full_path)
emoji.hash = emoji_data.get("hash", "")
if not emoji.hash:
logger.warning(f"[加载错误] 数据库记录缺少 'hash' 字段: {full_path}")
load_errors += 1
continue
emoji.description = emoji_data.get("description", "")
emoji.emotion = emoji_data.get("emotion", [])
emoji.usage_count = emoji_data.get("usage_count", 0)
last_used = emoji_data.get("last_used_time")
timestamp = emoji_data.get("timestamp")
emoji.last_used_time = (
last_used if last_used is not None else (timestamp if timestamp is not None else time.time())
)
emoji.register_time = timestamp if timestamp is not None else time.time()
emoji.format = emoji_data.get("format", "")
emoji_objects.append(emoji)
except ValueError as ve:
logger.error(f"[加载错误] 初始化 MaiEmoji 失败 ({full_path}): {ve}")
load_errors += 1
except Exception as e:
logger.error(f"[加载错误] 处理数据库记录时出错 ({full_path}): {str(e)}")
load_errors += 1
if load_errors > 0:
logger.warning(f"[查询] 加载过程中出现 {load_errors} 个错误。")
return emoji_objects return emoji_objects
except Exception as e: except Exception as e:
logger.error(f"[错误] 获取所有表情包对象失败: {str(e)}") logger.error(f"[错误] 从数据库获取表情包对象失败: {str(e)}")
return [] return []
async def get_emoji_from_manager(self, hash) -> MaiEmoji: async def get_emoji_from_manager(self, hash) -> Optional[MaiEmoji]:
"""EmojiManager中获取表情包 """内存中的 emoji_objects 列表获取表情包
参数: 参数:
hash:如果提供则只返回指定哈希值的表情包 hash: 要查找的表情包哈希值
返回:
MaiEmoji 或 None: 如果找到则返回 MaiEmoji 对象,否则返回 None
""" """
for emoji in self.emoji_objects: for emoji in self.emoji_objects:
if emoji.hash == hash: # 确保对象未被标记为删除且哈希值匹配
if not emoji.is_deleted and emoji.hash == hash:
return emoji return emoji
return None return None # 如果循环结束还没找到,则返回 None
async def delete_emoji(self, emoji_hash: str) -> bool: async def delete_emoji(self, emoji_hash: str) -> bool:
"""根据哈希值删除表情包 """根据哈希值删除表情包
@@ -656,11 +772,11 @@ class EmojiManager:
# 调用大模型进行决策 # 调用大模型进行决策
decision, _ = await self.llm_emotion_judge.generate_response_async(prompt, temperature=0.8) decision, _ = await self.llm_emotion_judge.generate_response_async(prompt, temperature=0.8)
logger.info(f"[决策] 大模型决策结果: {decision}") logger.info(f"[决策] 结果: {decision}")
# 解析决策结果 # 解析决策结果
if "不删除" in decision: if "不删除" in decision:
logger.info("[决策] 决定不删除任何表情包") logger.info("[决策] 不删除任何表情包")
return False return False
# 尝试从决策中提取表情包编号 # 尝试从决策中提取表情包编号
@@ -673,7 +789,7 @@ class EmojiManager:
emoji_to_delete = selected_emojis[emoji_index] emoji_to_delete = selected_emojis[emoji_index]
# 删除选定的表情包 # 删除选定的表情包
logger.info(f"[决策] 决定删除表情包: {emoji_to_delete.description}") logger.info(f"[决策] 删除表情包: {emoji_to_delete.description}")
delete_success = await self.delete_emoji(emoji_to_delete.hash) delete_success = await self.delete_emoji(emoji_to_delete.hash)
if delete_success: if delete_success:
@@ -682,7 +798,7 @@ class EmojiManager:
if register_success: if register_success:
self.emoji_objects.append(new_emoji) self.emoji_objects.append(new_emoji)
self.emoji_num += 1 self.emoji_num += 1
logger.success(f"[成功] 注册表情包: {new_emoji.description}") logger.success(f"[成功] 注册: {new_emoji.filename}")
return True return True
else: else:
logger.error(f"[错误] 注册表情包到数据库失败: {new_emoji.filename}") logger.error(f"[错误] 注册表情包到数据库失败: {new_emoji.filename}")
@@ -719,10 +835,10 @@ class EmojiManager:
# 调用AI获取描述 # 调用AI获取描述
if image_format == "gif" or image_format == "GIF": if image_format == "gif" or image_format == "GIF":
image_base64 = image_manager.transform_gif(image_base64) image_base64 = image_manager.transform_gif(image_base64)
prompt = "这是一个动态图表情包,每一张图代表了动态图的某一帧,黑色背景代表透明,详细描述一下表情包表达的情感和内容,请关注其幽默和讽刺意味" prompt = "这是一个动态图表情包,每一张图代表了动态图的某一帧,黑色背景代表透明,描述一下表情包表达的情感和内容,描述细节,从互联网梗,meme的角度去分析"
description, _ = await self.vlm.generate_response_for_image(prompt, image_base64, "jpg") description, _ = await self.vlm.generate_response_for_image(prompt, image_base64, "jpg")
else: else:
prompt = "这是一个表情包,请详细描述一下表情包所表达的情感和内容,请关注其幽默和讽刺意味" prompt = "这是一个表情包,请详细描述一下表情包所表达的情感和内容,描述细节,从互联网梗,meme的角度去分析"
description, _ = await self.vlm.generate_response_for_image(prompt, image_base64, image_format) description, _ = await self.vlm.generate_response_for_image(prompt, image_base64, image_format)
# 审核表情包 # 审核表情包
@@ -741,17 +857,22 @@ class EmojiManager:
# 分析情感含义 # 分析情感含义
emotion_prompt = f""" emotion_prompt = f"""
基于这个表情包的描述:'{description}'请列出1-2个可能的情感标签每个标签用一个词组表示格式如下 请你识别这个表情包的含义和适用场景给我简短的描述每个描述不要超过15个字
幽默的讽刺 这是一个基于这个表情包的描述:'{description}'
悲伤的无奈 你可以关注其幽默和讽刺意味,动用贴吧,微博,小红书的知识,必须从互联网梗,meme的角度去分析
愤怒的抗议 请直接输出描述,不要出现任何其他内容,如果有多个描述,可以用逗号分隔
愤怒的讽刺 """
直接输出词组,词组检用逗号分隔。"""
emotions_text, _ = await self.llm_emotion_judge.generate_response_async(emotion_prompt, temperature=0.7) emotions_text, _ = await self.llm_emotion_judge.generate_response_async(emotion_prompt, temperature=0.7)
# 处理情感列表 # 处理情感列表
emotions = [e.strip() for e in emotions_text.split(",") if e.strip()] emotions = [e.strip() for e in emotions_text.split(",") if e.strip()]
# 根据情感标签数量随机选择喵~超过5个选3个超过2个选2个
if len(emotions) > 5:
emotions = random.sample(emotions, 3)
elif len(emotions) > 2:
emotions = random.sample(emotions, 2)
return f"[表情包:{description}]", emotions return f"[表情包:{description}]", emotions
except Exception as e: except Exception as e:
@@ -767,100 +888,176 @@ class EmojiManager:
Returns: Returns:
bool: 注册是否成功 bool: 注册是否成功
""" """
file_full_path = os.path.join(EMOJI_DIR, filename)
if not os.path.exists(file_full_path):
logger.error(f"[注册失败] 文件不存在: {file_full_path}")
return False
try: try:
# 使用MaiEmoji类创建表情包实例 # 1. 创建 MaiEmoji 实例并初始化哈希和格式
new_emoji = MaiEmoji(filename, EMOJI_DIR) new_emoji = MaiEmoji(full_path=file_full_path)
await new_emoji.initialize_hash_format() init_result = await new_emoji.initialize_hash_format()
emoji_base64 = image_path_to_base64(os.path.join(EMOJI_DIR, filename)) if init_result is None or new_emoji.is_deleted: # 初始化失败或文件读取错误
description, emotions = await self.build_emoji_description(emoji_base64) logger.error(f"[注册失败] 初始化哈希和格式失败: {filename}")
if description == "" or description == None: # 是否需要删除源文件?看业务需求,暂时不删
return False return False
new_emoji.description = description
new_emoji.emotion = emotions
# 检查是否已经注册过 # 2. 检查哈希是否已存在 (在内存中检查)
# 对比内存中是否存在相同哈希值的表情包
if await self.get_emoji_from_manager(new_emoji.hash): if await self.get_emoji_from_manager(new_emoji.hash):
logger.warning(f"[警告] 表情包已存在: {filename}") logger.warning(f"[注册跳过] 表情包已存在 (Hash: {new_emoji.hash}): {filename}")
# 删除重复的源文件
try:
os.remove(file_full_path)
logger.info(f"[清理] 删除重复的待注册文件: {filename}")
except Exception as e:
logger.error(f"[错误] 删除重复文件失败: {str(e)}")
return False # 返回 False 表示未注册新表情
# 3. 构建描述和情感
try:
emoji_base64 = image_path_to_base64(file_full_path)
if emoji_base64 is None: # 再次检查读取
logger.error(f"[注册失败] 无法读取图片以生成描述: {filename}")
return False
description, emotions = await self.build_emoji_description(emoji_base64)
if not description: # 检查描述是否成功生成或审核通过
logger.warning(f"[注册失败] 未能生成有效描述或审核未通过: {filename}")
# 删除未能生成描述的文件
try:
os.remove(file_full_path)
logger.info(f"[清理] 删除描述生成失败的文件: {filename}")
except Exception as e:
logger.error(f"[错误] 删除描述生成失败文件时出错: {str(e)}")
return False
new_emoji.description = description
new_emoji.emotion = emotions
except Exception as build_desc_error:
logger.error(f"[注册失败] 生成描述/情感时出错 ({filename}): {build_desc_error}")
# 同样考虑删除文件
try:
os.remove(file_full_path)
logger.info(f"[清理] 删除描述生成异常的文件: {filename}")
except Exception as e:
logger.error(f"[错误] 删除描述生成异常文件时出错: {str(e)}")
return False return False
# 4. 检查容量并决定是否替换或直接注册
if self.emoji_num >= self.emoji_num_max: if self.emoji_num >= self.emoji_num_max:
logger.warning(f"表情包数量已达到上限({self.emoji_num}/{self.emoji_num_max})") logger.warning(f"表情包数量已达到上限({self.emoji_num}/{self.emoji_num_max}),尝试替换...")
replaced = await self.replace_a_emoji(new_emoji) replaced = await self.replace_a_emoji(new_emoji)
if not replaced: if not replaced:
logger.error("[错误] 替换表情包失败,无法完成注册") logger.error("[注册失败] 替换表情包失败,无法完成注册")
# 替换失败,删除新表情包文件
try:
os.remove(file_full_path) # new_emoji 的 full_path 此时还是源路径
logger.info(f"[清理] 删除替换失败的新表情文件: {filename}")
except Exception as e:
logger.error(f"[错误] 删除替换失败文件时出错: {str(e)}")
return False return False
# 替换成功时replace_a_emoji 内部已处理 new_emoji 的注册和添加到列表
return True return True
else: else:
# 修复:等待异步注册完成 # 直接注册
register_success = await new_emoji.register_to_db() register_success = await new_emoji.register_to_db() # 此方法会移动文件并更新 DB
if register_success: if register_success:
# 注册成功后,添加到内存列表
self.emoji_objects.append(new_emoji) self.emoji_objects.append(new_emoji)
self.emoji_num += 1 self.emoji_num += 1
logger.success(f"[成功] 注册表情包: {filename}") logger.success(f"[成功] 注册表情包: {filename} (当前: {self.emoji_num}/{self.emoji_num_max})")
return True return True
else: else:
logger.error(f"[错误] 注册表情包到数据库失败: {filename}") logger.error(f"[注册失败] 保存表情包到数据库/移动文件失败: {filename}")
# register_to_db 失败时,内部会尝试清理移动后的文件,源文件可能还在
# 是否需要删除源文件?
if os.path.exists(file_full_path):
try:
os.remove(file_full_path)
logger.info(f"[清理] 删除注册失败的源文件: {filename}")
except Exception as e:
logger.error(f"[错误] 删除注册失败源文件时出错: {str(e)}")
return False return False
except Exception as e: except Exception as e:
logger.error(f"[错误] 注册表情包失败: {str(e)}") logger.error(f"[错误] 注册表情包时发生未预期错误 ({filename}): {str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
# 尝试删除源文件以避免循环处理
if os.path.exists(file_full_path):
try:
os.remove(file_full_path)
logger.info(f"[清理] 删除处理异常的源文件: {filename}")
except Exception as remove_error:
logger.error(f"[错误] 删除异常处理文件时出错: {remove_error}")
return False return False
async def clear_temp_emoji(self): async def clear_temp_emoji(self):
"""每天清理临时表情包 """清理临时表情包
清理/data/emoji和/data/image目录下的所有文件 清理/data/emoji和/data/image目录下的所有文件
当目录中文件数超过50时会全部删除 当目录中文件数超过100时会全部删除
""" """
logger.info("[清理] 开始清理临时表情包...") logger.info("[清理] 开始清理缓存...")
# 清理emoji目录 # 清理emoji目录
emoji_dir = os.path.join(BASE_DIR, "emoji") emoji_dir = os.path.join(BASE_DIR, "emoji")
if os.path.exists(emoji_dir): if os.path.exists(emoji_dir):
files = os.listdir(emoji_dir) files = os.listdir(emoji_dir)
# 如果文件数超过50就全部删除 # 如果文件数超过50就全部删除
if len(files) > 50: if len(files) > 100:
for filename in files: for filename in files:
file_path = os.path.join(emoji_dir, filename) file_path = os.path.join(emoji_dir, filename)
if os.path.isfile(file_path): if os.path.isfile(file_path):
os.remove(file_path) os.remove(file_path)
logger.debug(f"[清理] 删除表情包文件: {filename}") logger.debug(f"[清理] 删除: {filename}")
# 清理image目录 # 清理image目录
image_dir = os.path.join(BASE_DIR, "image") image_dir = os.path.join(BASE_DIR, "image")
if os.path.exists(image_dir): if os.path.exists(image_dir):
files = os.listdir(image_dir) files = os.listdir(image_dir)
# 如果文件数超过50就全部删除 # 如果文件数超过50就全部删除
if len(files) > 50: if len(files) > 100:
for filename in files: for filename in files:
file_path = os.path.join(image_dir, filename) file_path = os.path.join(image_dir, filename)
if os.path.isfile(file_path): if os.path.isfile(file_path):
os.remove(file_path) os.remove(file_path)
logger.debug(f"[清理] 删除图片文件: {filename}") logger.debug(f"[清理] 删除图片: {filename}")
logger.success("[清理] 临时文件清理完成") logger.success("[清理] 完成")
async def clean_unused_emojis(self, emoji_dir, emoji_objects): async def clean_unused_emojis(self, emoji_dir, emoji_objects):
"""清理未使用的表情包文件 """清理指定目录中未被 emoji_objects 追踪的表情包文件"""
遍历指定文件夹中的所有文件删除未在emoji_objects列表中的文件 if not os.path.exists(emoji_dir):
""" logger.warning(f"[清理] 目标目录不存在,跳过清理: {emoji_dir}")
# 获取所有表情包路径 return
emoji_paths = {emoji.path for emoji in emoji_objects}
# 遍历文件夹中的所有文件 try:
for file_name in os.listdir(emoji_dir): # 获取内存中所有有效表情包的完整路径集合
file_path = os.path.join(emoji_dir, file_name) tracked_full_paths = {emoji.full_path for emoji in emoji_objects if not emoji.is_deleted}
cleaned_count = 0
# 检查文件是否在表情包路径列表中 # 遍历指定目录中的所有文件
if file_path not in emoji_paths: for file_name in os.listdir(emoji_dir):
try: file_full_path = os.path.join(emoji_dir, file_name)
# 删除未在表情包列表中的文件
os.remove(file_path) # 确保处理的是文件而不是子目录
logger.info(f"[清理] 删除未使用的表情包文件: {file_path}") if not os.path.isfile(file_full_path):
except Exception as e: continue
logger.error(f"[错误] 删除文件时出错: {str(e)}")
# 如果文件不在被追踪的集合中,则删除
if file_full_path not in tracked_full_paths:
try:
os.remove(file_full_path)
logger.info(f"[清理] 删除未追踪的表情包文件: {file_full_path}")
cleaned_count += 1
except Exception as e:
logger.error(f"[错误] 删除文件时出错 ({file_full_path}): {str(e)}")
if cleaned_count > 0:
logger.success(f"[清理] 在目录 {emoji_dir} 中清理了 {cleaned_count} 个破损表情包。")
else:
logger.info(f"[清理] 目录 {emoji_dir} 中没有需要清理的。")
except Exception as e:
logger.error(f"[错误] 清理未使用表情包文件时出错 ({emoji_dir}): {str(e)}")
# 创建全局单例 # 创建全局单例

View File

@@ -2,6 +2,7 @@ import asyncio
import time import time
import traceback import traceback
import random # <--- 添加导入 import random # <--- 添加导入
import json # <--- 确保导入 json
from typing import List, Optional, Dict, Any, Deque, Callable, Coroutine from typing import List, Optional, Dict, Any, Deque, Callable, Coroutine
from collections import deque from collections import deque
from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending
@@ -14,9 +15,7 @@ from src.plugins.models.utils_model import LLMRequest
from src.config.config import global_config from src.config.config import global_config
from src.plugins.chat.utils_image import image_path_to_base64 # Local import needed after move from src.plugins.chat.utils_image import image_path_to_base64 # Local import needed after move
from src.plugins.utils.timer_calculator import Timer # <--- Import Timer from src.plugins.utils.timer_calculator import Timer # <--- Import Timer
from src.do_tool.tool_use import ToolUser
from src.plugins.emoji_system.emoji_manager import emoji_manager from src.plugins.emoji_system.emoji_manager import emoji_manager
from src.plugins.utils.json_utils import process_llm_tool_calls, extract_tool_call_arguments
from src.heart_flow.sub_mind import SubMind from src.heart_flow.sub_mind import SubMind
from src.heart_flow.observation import Observation from src.heart_flow.observation import Observation
from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_manager, prompt_builder from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_manager, prompt_builder
@@ -30,12 +29,14 @@ from src.plugins.moods.moods import MoodManager
from src.individuality.individuality import Individuality from src.individuality.individuality import Individuality
INITIAL_DURATION = 60.0
WAITING_TIME_THRESHOLD = 300 # 等待新消息时间阈值,单位秒 WAITING_TIME_THRESHOLD = 300 # 等待新消息时间阈值,单位秒
EMOJI_SEND_PRO = 0.3 # 设置一个概率,比如 30% 才真的发
logger = get_logger("interest") # Logger Name Changed CONSECUTIVE_NO_REPLY_THRESHOLD = 3 # 连续不回复的阈值
logger = get_logger("hfc") # Logger Name Changed
# 默认动作定义 # 默认动作定义
@@ -117,35 +118,6 @@ class ActionManager:
"""重置为默认动作集""" """重置为默认动作集"""
self._available_actions = DEFAULT_ACTIONS.copy() self._available_actions = DEFAULT_ACTIONS.copy()
def get_planner_tool_definition(self) -> List[Dict[str, Any]]:
"""获取当前动作集对应的规划器工具定义"""
return [
{
"type": "function",
"function": {
"name": "decide_reply_action",
"description": "根据当前聊天内容和上下文,决定机器人是否应该回复以及如何回复。",
"parameters": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": list(self._available_actions.keys()),
"description": "决定采取的行动:"
+ ", ".join([f"'{k}'({v})" for k, v in self._available_actions.items()]),
},
"reasoning": {"type": "string", "description": "做出此决定的简要理由。"},
"emoji_query": {
"type": "string",
"description": "如果行动是'emoji_reply',指定表情的主题或概念。如果行动是'text_reply'且希望在文本后追加表情,也在此指定表情主题。",
},
},
"required": ["action", "reasoning"],
},
},
}
]
# 在文件开头添加自定义异常类 # 在文件开头添加自定义异常类
class HeartFCError(Exception): class HeartFCError(Exception):
@@ -179,8 +151,6 @@ class HeartFChatting:
其生命周期现在由其关联的 SubHeartflow 的 FOCUSED 状态控制。 其生命周期现在由其关联的 SubHeartflow 的 FOCUSED 状态控制。
""" """
CONSECUTIVE_NO_REPLY_THRESHOLD = 3 # 连续不回复的阈值
def __init__( def __init__(
self, self,
chat_id: str, chat_id: str,
@@ -222,7 +192,6 @@ class HeartFChatting:
max_tokens=256, max_tokens=256,
request_type="response_heartflow", request_type="response_heartflow",
) )
self.tool_user = ToolUser()
self.heart_fc_sender = HeartFCSender() self.heart_fc_sender = HeartFCSender()
# LLM规划器配置 # LLM规划器配置
@@ -261,7 +230,7 @@ class HeartFChatting:
self.log_prefix = f"[{chat_manager.get_stream_name(self.stream_id) or self.stream_id}]" self.log_prefix = f"[{chat_manager.get_stream_name(self.stream_id) or self.stream_id}]"
self._initialized = True self._initialized = True
logger.info(f"麦麦感觉到了,可以开始认真水群{self.log_prefix} ") logger.debug(f"{self.log_prefix}麦麦感觉到了,可以开始认真水群 ")
return True return True
async def start(self): async def start(self):
@@ -292,7 +261,7 @@ class HeartFChatting:
pass # 忽略取消或超时错误 pass # 忽略取消或超时错误
self._loop_task = None # 清理旧任务引用 self._loop_task = None # 清理旧任务引用
logger.info(f"{self.log_prefix} 启动认真水群(HFC)主循环...") logger.debug(f"{self.log_prefix} 启动认真水群(HFC)主循环...")
# 创建新的循环任务 # 创建新的循环任务
self._loop_task = asyncio.create_task(self._hfc_loop()) self._loop_task = asyncio.create_task(self._hfc_loop())
# 添加完成回调 # 添加完成回调
@@ -470,6 +439,16 @@ class HeartFChatting:
# execute:执行 # execute:执行
# 在此处添加日志记录
if action == "text_reply":
action_str = "回复"
elif action == "emoji_reply":
action_str = "回复表情"
else:
action_str = "不回复"
logger.info(f"{self.log_prefix} 麦麦决定'{action_str}', 原因'{reasoning}'")
return await self._handle_action( return await self._handle_action(
action, reasoning, planner_result.get("emoji_query", ""), cycle_timers, planner_start_db_time action, reasoning, planner_result.get("emoji_query", ""), cycle_timers, planner_start_db_time
) )
@@ -644,14 +623,14 @@ class HeartFChatting:
self._lian_xu_bu_hui_fu_ci_shu += 1 self._lian_xu_bu_hui_fu_ci_shu += 1
self._lian_xu_deng_dai_shi_jian += dang_qian_deng_dai # 累加等待时间 self._lian_xu_deng_dai_shi_jian += dang_qian_deng_dai # 累加等待时间
logger.debug( logger.debug(
f"{self.log_prefix} 连续不回复计数增加: {self._lian_xu_bu_hui_fu_ci_shu}/{self.CONSECUTIVE_NO_REPLY_THRESHOLD}, " f"{self.log_prefix} 连续不回复计数增加: {self._lian_xu_bu_hui_fu_ci_shu}/{CONSECUTIVE_NO_REPLY_THRESHOLD}, "
f"本次等待: {dang_qian_deng_dai:.2f}秒, 累计等待: {self._lian_xu_deng_dai_shi_jian:.2f}" f"本次等待: {dang_qian_deng_dai:.2f}秒, 累计等待: {self._lian_xu_deng_dai_shi_jian:.2f}"
) )
# 检查是否同时达到次数和时间阈值 # 检查是否同时达到次数和时间阈值
time_threshold = 0.66 * WAITING_TIME_THRESHOLD * self.CONSECUTIVE_NO_REPLY_THRESHOLD time_threshold = 0.66 * WAITING_TIME_THRESHOLD * CONSECUTIVE_NO_REPLY_THRESHOLD
if ( if (
self._lian_xu_bu_hui_fu_ci_shu >= self.CONSECUTIVE_NO_REPLY_THRESHOLD self._lian_xu_bu_hui_fu_ci_shu >= CONSECUTIVE_NO_REPLY_THRESHOLD
and self._lian_xu_deng_dai_shi_jian >= time_threshold and self._lian_xu_deng_dai_shi_jian >= time_threshold
): ):
logger.info( logger.info(
@@ -661,7 +640,7 @@ class HeartFChatting:
) )
# 调用回调。注意:这里不重置计数器和时间,依赖回调函数成功改变状态来隐式重置上下文。 # 调用回调。注意:这里不重置计数器和时间,依赖回调函数成功改变状态来隐式重置上下文。
await self.on_consecutive_no_reply_callback() await self.on_consecutive_no_reply_callback()
elif self._lian_xu_bu_hui_fu_ci_shu >= self.CONSECUTIVE_NO_REPLY_THRESHOLD: elif self._lian_xu_bu_hui_fu_ci_shu >= CONSECUTIVE_NO_REPLY_THRESHOLD:
# 仅次数达到阈值,但时间未达到 # 仅次数达到阈值,但时间未达到
logger.debug( logger.debug(
f"{self.log_prefix} 连续不回复次数达到阈值 ({self._lian_xu_bu_hui_fu_ci_shu}次) " f"{self.log_prefix} 连续不回复次数达到阈值 ({self._lian_xu_bu_hui_fu_ci_shu}次) "
@@ -784,41 +763,36 @@ class HeartFChatting:
async def _planner(self, current_mind: str, cycle_timers: dict, is_re_planned: bool = False) -> Dict[str, Any]: async def _planner(self, current_mind: str, cycle_timers: dict, is_re_planned: bool = False) -> Dict[str, Any]:
""" """
规划器 (Planner): 使用LLM根据上下文决定是否和如何回复。 规划器 (Planner): 使用LLM根据上下文决定是否和如何回复。
重构为让LLM返回结构化JSON文本然后在代码中解析。
参数: 参数:
current_mind: 子思维的当前思考结果 current_mind: 子思维的当前思考结果
cycle_timers: 计时器字典 cycle_timers: 计时器字典
is_re_planned: 是否为重新规划 is_re_planned: 是否为重新规划 (此重构中暂时简化,不处理 is_re_planned 的特殊逻辑)
""" """
logger.info(f"{self.log_prefix}[Planner] 开始{'重新' if is_re_planned else ''}执行规划器") logger.info(f"{self.log_prefix}开始想要做什么")
# --- 新增:检查历史动作并调整可用动作 ---
lian_xu_wen_ben_hui_fu = 0 # 连续文本回复次数
actions_to_remove_temporarily = [] actions_to_remove_temporarily = []
probability_roll = random.random() # 在循环外掷骰子一次,用于概率判断 # --- 检查历史动作并决定临时移除动作 (逻辑保持不变) ---
lian_xu_wen_ben_hui_fu = 0
# 反向遍历最近的循环历史 probability_roll = random.random()
for cycle in reversed(self._cycle_history): for cycle in reversed(self._cycle_history):
# 只关心实际执行了动作的循环
if cycle.action_taken: if cycle.action_taken:
if cycle.action_type == "text_reply": if cycle.action_type == "text_reply":
lian_xu_wen_ben_hui_fu += 1 lian_xu_wen_ben_hui_fu += 1
else: else:
break # 遇到非文本回复,中断计数 break
# 检查最近的3个循环即可避免检查过多历史 (如果历史很长)
if len(self._cycle_history) > 0 and cycle.cycle_id <= self._cycle_history[0].cycle_id + ( if len(self._cycle_history) > 0 and cycle.cycle_id <= self._cycle_history[0].cycle_id + (
len(self._cycle_history) - 4 len(self._cycle_history) - 4
): ):
break break
logger.debug(f"{self.log_prefix}[Planner] 检测到连续文本回复次数: {lian_xu_wen_ben_hui_fu}") logger.debug(f"{self.log_prefix}[Planner] 检测到连续文本回复次数: {lian_xu_wen_ben_hui_fu}")
# 根据连续次数决定临时移除哪些动作
if lian_xu_wen_ben_hui_fu >= 3: if lian_xu_wen_ben_hui_fu >= 3:
logger.info(f"{self.log_prefix}[Planner] 连续回复 >= 3 次,强制移除 text_reply 和 emoji_reply") logger.info(f"{self.log_prefix}[Planner] 连续回复 >= 3 次,强制移除 text_reply 和 emoji_reply")
actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"]) actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"])
elif lian_xu_wen_ben_hui_fu == 2: elif lian_xu_wen_ben_hui_fu == 2:
if probability_roll < 0.8: # 80% 概率 if probability_roll < 0.8:
logger.info(f"{self.log_prefix}[Planner] 连续回复 2 次80% 概率移除 text_reply 和 emoji_reply (触发)") logger.info(f"{self.log_prefix}[Planner] 连续回复 2 次80% 概率移除 text_reply 和 emoji_reply (触发)")
actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"]) actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"])
else: else:
@@ -826,168 +800,179 @@ class HeartFChatting:
f"{self.log_prefix}[Planner] 连续回复 2 次80% 概率移除 text_reply 和 emoji_reply (未触发)" f"{self.log_prefix}[Planner] 连续回复 2 次80% 概率移除 text_reply 和 emoji_reply (未触发)"
) )
elif lian_xu_wen_ben_hui_fu == 1: elif lian_xu_wen_ben_hui_fu == 1:
if probability_roll < 0.4: # 40% 概率 if probability_roll < 0.4:
logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次40% 概率移除 text_reply (触发)") logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次40% 概率移除 text_reply (触发)")
actions_to_remove_temporarily.append("text_reply") actions_to_remove_temporarily.append("text_reply")
else: else:
logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次40% 概率移除 text_reply (未触发)") logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次40% 概率移除 text_reply (未触发)")
# 如果 lian_xu_wen_ben_hui_fu == 0则不移除任何动作 # --- 结束检查历史动作 ---
# --- 结束:检查历史动作 ---
# 获取观察信息 # 获取观察信息
observation = self.observations[0] observation = self.observations[0]
if is_re_planned: # if is_re_planned: # 暂时简化,不处理重新规划
await observation.observe() # await observation.observe()
observed_messages = observation.talking_message observed_messages = observation.talking_message
observed_messages_str = observation.talking_message_str_truncate observed_messages_str = observation.talking_message_str_truncate
# --- 使用 LLM 进行决策 --- # # --- 使用 LLM 进行决策 (JSON 输出模式) --- #
reasoning = "默认决策或获取决策失败" action = "no_reply" # 默认动作
llm_error = False # LLM错误标志 reasoning = "规划器初始化默认"
arguments = None # 初始化参数变量 emoji_query = ""
emoji_query = "" # <--- 在这里初始化 emoji_query llm_error = False # LLM 请求或解析错误标志
# 获取我们将传递给 prompt 构建器和用于验证的当前可用动作
current_available_actions = self.action_manager.get_available_actions()
try: try:
# --- 新增:应用临时动作移除 --- # --- 应用临时动作移除 ---
if actions_to_remove_temporarily: if actions_to_remove_temporarily:
self.action_manager.temporarily_remove_actions(actions_to_remove_temporarily) self.action_manager.temporarily_remove_actions(actions_to_remove_temporarily)
# 更新 current_available_actions 以反映移除后的状态
current_available_actions = self.action_manager.get_available_actions()
logger.debug( logger.debug(
f"{self.log_prefix}[Planner] 临时移除的动作: {actions_to_remove_temporarily}, 当前可用: {list(self.action_manager.get_available_actions().keys())}" f"{self.log_prefix}[Planner] 临时移除的动作: {actions_to_remove_temporarily}, 当前可用: {list(current_available_actions.keys())}"
) )
# --- 构建提示词 --- # --- 构建提示词 (调用修改后的 _build_planner_prompt) ---
replan_prompt_str = "" # replan_prompt_str = "" # 暂时简化
if is_re_planned: # if is_re_planned:
replan_prompt_str = await self._build_replan_prompt( # replan_prompt_str = await self._build_replan_prompt(
self._current_cycle.action_type, self._current_cycle.reasoning # self._current_cycle.action_type, self._current_cycle.reasoning
) # )
prompt = await self._build_planner_prompt( prompt = await self._build_planner_prompt(
observed_messages_str, current_mind, self.sub_mind.structured_info, replan_prompt_str observed_messages_str,
current_mind,
self.sub_mind.structured_info,
"", # replan_prompt_str,
current_available_actions, # <--- 传入当前可用动作
) )
# --- 调用 LLM --- # --- 调用 LLM (普通文本生成) ---
llm_content = None
try: try:
planner_tools = self.action_manager.get_planner_tool_definition() # 假设 LLMRequest 有 generate_response 方法返回 (content, reasoning, model_name)
logger.debug(f"{self.log_prefix}[Planner] 本次使用的工具定义: {planner_tools}") # 记录本次使用的工具 # 我们只需要 content
_response_text, _reasoning_content, tool_calls = await self.planner_llm.generate_response_tool_async( # !! 注意:这里假设 self.planner_llmgenerate_response 方法
prompt=prompt, # !! 如果你的 LLMRequest 类使用的是其他方法名,请相应修改
tools=planner_tools, llm_content, _, _ = await self.planner_llm.generate_response(prompt=prompt)
) logger.debug(f"{self.log_prefix}[Planner] LLM 原始 JSON 响应 (预期): {llm_content}")
logger.debug(f"{self.log_prefix}[Planner] 原始人 LLM响应: {_response_text}")
except Exception as req_e: except Exception as req_e:
logger.error(f"{self.log_prefix}[Planner] LLM请求执行失败: {req_e}") logger.error(f"{self.log_prefix}[Planner] LLM 请求执行失败: {req_e}")
action = "error" reasoning = f"LLM 请求失败: {req_e}"
reasoning = f"LLM请求失败: {req_e}"
llm_error = True llm_error = True
# 直接返回错误结果 # 直接使用默认动作返回错误结果
return { action = "no_reply" # 明确设置为默认值
"action": action, emoji_query = "" # 明确设置为空
"reasoning": reasoning, # 不再立即返回,而是继续执行 finally 块以恢复动作
"emoji_query": "", # return { ... }
"current_mind": current_mind,
"observed_messages": observed_messages,
"llm_error": llm_error,
}
# 默认错误状态 # --- 解析 LLM 返回的 JSON (仅当 LLM 请求未出错时进行) ---
action = "error" if not llm_error and llm_content:
reasoning = "处理工具调用时出错" try:
llm_error = True # 尝试去除可能的 markdown 代码块标记
cleaned_content = (
llm_content.strip().removeprefix("```json").removeprefix("```").removesuffix("```").strip()
)
if not cleaned_content:
raise json.JSONDecodeError("Cleaned content is empty", cleaned_content, 0)
parsed_json = json.loads(cleaned_content)
# 1. 验证工具调用 # 提取决策,提供默认值
success, valid_tool_calls, error_msg = process_llm_tool_calls( extracted_action = parsed_json.get("action", "no_reply")
tool_calls, log_prefix=f"{self.log_prefix}[Planner] " extracted_reasoning = parsed_json.get("reasoning", "LLM未提供理由")
) extracted_emoji_query = parsed_json.get("emoji_query", "")
if success and valid_tool_calls: # 验证动作是否在当前可用列表中
# 2. 提取第一个调用并获取参数 # !! 使用调用 prompt 时实际可用的动作列表进行验证
first_tool_call = valid_tool_calls[0] if extracted_action not in current_available_actions:
tool_name = first_tool_call.get("function", {}).get("name")
arguments = extract_tool_call_arguments(first_tool_call, None)
# 3. 检查名称和参数
expected_tool_name = "decide_reply_action"
if tool_name == expected_tool_name and arguments is not None:
# 4. 成功,提取决策
extracted_action = arguments.get("action", "no_reply")
# 验证动作
if extracted_action not in self.action_manager.get_available_actions():
# 如果LLM返回了一个此时不该用的动作因为被临时移除了
# 或者完全无效的动作
logger.warning( logger.warning(
f"{self.log_prefix}[Planner] LLM返回了当前不可用或无效的动作: {extracted_action},将强制使用 'no_reply'" f"{self.log_prefix}[Planner] LLM 返回了当前不可用或无效的动作: '{extracted_action}' (可用: {list(current_available_actions.keys())}),将强制使用 'no_reply'"
) )
action = "no_reply" action = "no_reply"
reasoning = f"LLM返回了当前不可用的动作: {extracted_action}" reasoning = f"LLM 返回了当前不可用的动作 '{extracted_action}' (可用: {list(current_available_actions.keys())})。原始理由: {extracted_reasoning}"
emoji_query = "" emoji_query = ""
llm_error = False # 视为逻辑修正而非 LLM 错误 # 检查 no_reply 是否也恰好被移除了 (极端情况)
# --- 检查 'no_reply' 是否也恰好被移除了 (极端情况) --- if "no_reply" not in current_available_actions:
if "no_reply" not in self.action_manager.get_available_actions():
logger.error( logger.error(
f"{self.log_prefix}[Planner] 严重错误:'no_reply' 动作也不可用!无法执行任何动作。" f"{self.log_prefix}[Planner] 严重错误:'no_reply' 动作也不可用!无法执行任何动作。"
) )
action = "error" # 回退到错误状态 action = "error" # 回退到错误状态
reasoning = "无法执行任何有效动作,包括 no_reply" reasoning = "无法执行任何有效动作,包括 no_reply"
llm_error = True llm_error = True # 标记为严重错误
else:
llm_error = False # 视为逻辑修正而非 LLM 错误
else: else:
# 动作有效且可用,使用提取的值 # 动作有效且可用
action = extracted_action action = extracted_action
reasoning = arguments.get("reasoning", "未提供理由") reasoning = extracted_reasoning
emoji_query = arguments.get("emoji_query", "") emoji_query = extracted_emoji_query
llm_error = False # 成功处理 llm_error = False # 解析成功
# 记录决策结果
logger.debug( logger.debug(
f"{self.log_prefix}[要做什么]\nPrompt:\n{prompt}\n\n决策结果: {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'" f"{self.log_prefix}[要做什么]\nPrompt:\n{prompt}\n\n决策结果 (来自JSON): {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'"
) )
elif tool_name != expected_tool_name:
reasoning = f"LLM返回了非预期的工具: {tool_name}"
logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
else: # arguments is None
reasoning = f"无法提取工具 {tool_name} 的参数"
logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
elif not success:
reasoning = f"验证工具调用失败: {error_msg}"
logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
else: # not valid_tool_calls
# 如果没有有效的工具调用,我们需要检查 'no_reply' 是否是当前唯一可用的动作
available_actions = list(self.action_manager.get_available_actions().keys())
if available_actions == ["no_reply"]:
logger.info(
f"{self.log_prefix}[Planner] LLM未返回工具调用但当前唯一可用动作是 'no_reply',将执行 'no_reply'"
)
action = "no_reply"
reasoning = "LLM未返回工具调用且当前仅 'no_reply' 可用"
emoji_query = ""
llm_error = False # 视为逻辑选择而非错误
else:
reasoning = "LLM未返回有效的工具调用"
logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
# llm_error 保持为 True
# 如果 llm_error 仍然是 True说明在处理过程中有错误发生
except Exception as llm_e: except json.JSONDecodeError as json_e:
logger.error(f"{self.log_prefix}[Planner] Planner LLM处理过程中发生意外错误: {llm_e}") logger.warning(
f"{self.log_prefix}[Planner] 解析LLM响应JSON失败: {json_e}. LLM原始输出: '{llm_content}'"
)
reasoning = f"解析LLM响应JSON失败: {json_e}. 将使用默认动作 'no_reply'."
action = "no_reply" # 解析失败则默认不回复
emoji_query = ""
llm_error = True # 标记解析错误
except Exception as parse_e:
logger.error(f"{self.log_prefix}[Planner] 处理LLM响应时发生意外错误: {parse_e}")
reasoning = f"处理LLM响应时发生意外错误: {parse_e}. 将使用默认动作 'no_reply'."
action = "no_reply"
emoji_query = ""
llm_error = True
elif not llm_error and not llm_content:
# LLM 请求成功但返回空内容
logger.warning(f"{self.log_prefix}[Planner] LLM 返回了空内容。")
reasoning = "LLM 返回了空内容,使用默认动作 'no_reply'."
action = "no_reply"
emoji_query = ""
llm_error = True # 标记为空响应错误
# 如果 llm_error 在此阶段为 True意味着请求成功但解析失败或返回空
# 如果 llm_error 在请求阶段就为 True则跳过了此解析块
except Exception as outer_e:
logger.error(f"{self.log_prefix}[Planner] Planner 处理过程中发生意外错误: {outer_e}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
action = "error" action = "error" # 发生未知错误,标记为 error 动作
reasoning = f"Planner内部处理错误: {llm_e}" reasoning = f"Planner 内部处理错误: {outer_e}"
emoji_query = ""
llm_error = True llm_error = True
# --- 新增:确保动作恢复 ---
finally: finally:
if actions_to_remove_temporarily: # 只有当确实移除了动作时才需要恢复 # --- 确保动作恢复 ---
# 检查 self._original_actions_backup 是否有值来判断是否需要恢复
if self.action_manager._original_actions_backup is not None:
self.action_manager.restore_actions() self.action_manager.restore_actions()
logger.debug( logger.debug(
f"{self.log_prefix}[Planner] 恢复了原始动作集, 当前可用: {list(self.action_manager.get_available_actions().keys())}" f"{self.log_prefix}[Planner] 恢复了原始动作集, 当前可用: {list(self.action_manager.get_available_actions().keys())}"
) )
# --- 结束确保动作恢复 --- # --- 结束确保动作恢复 ---
# --- 结束 LLM 决策 --- #
# --- 概率性忽略文本回复附带的表情 (逻辑保持不变) ---
if action == "text_reply" and emoji_query:
logger.debug(f"{self.log_prefix}[Planner] 大模型建议文字回复带表情: '{emoji_query}'")
if random.random() > EMOJI_SEND_PRO:
logger.info(
f"{self.log_prefix}但是麦麦这次不想加表情 ({1 - EMOJI_SEND_PRO:.0%}),忽略表情 '{emoji_query}'"
)
emoji_query = "" # 清空表情请求
else:
logger.info(f"{self.log_prefix}好吧,加上表情 '{emoji_query}'")
# --- 结束概率性忽略 ---
# 返回结果字典
return { return {
"action": action, "action": action,
"reasoning": reasoning, "reasoning": reasoning,
"emoji_query": emoji_query, "emoji_query": emoji_query,
"current_mind": current_mind, "current_mind": current_mind,
"observed_messages": observed_messages, "observed_messages": observed_messages,
"llm_error": llm_error, "llm_error": llm_error, # 返回错误状态
} }
async def _get_anchor_message(self) -> Optional[MessageRecv]: async def _get_anchor_message(self) -> Optional[MessageRecv]:
@@ -1016,9 +1001,7 @@ class HeartFChatting:
} }
anchor_message = MessageRecv(placeholder_msg_dict) anchor_message = MessageRecv(placeholder_msg_dict)
anchor_message.update_chat_stream(self.chat_stream) anchor_message.update_chat_stream(self.chat_stream)
logger.info( logger.debug(f"{self.log_prefix} 创建占位符锚点消息: ID={anchor_message.message_info.message_id}")
f"{self.log_prefix} Created placeholder anchor message: ID={anchor_message.message_info.message_id}"
)
return anchor_message return anchor_message
except Exception as e: except Exception as e:
@@ -1131,8 +1114,9 @@ class HeartFChatting:
current_mind: Optional[str], current_mind: Optional[str],
structured_info: Dict[str, Any], structured_info: Dict[str, Any],
replan_prompt: str, replan_prompt: str,
current_available_actions: Dict[str, str],
) -> str: ) -> str:
"""构建 Planner LLM 的提示词""" """构建 Planner LLM 的提示词 (获取模板并填充数据)"""
try: try:
# 准备结构化信息块 # 准备结构化信息块
structured_info_block = "" structured_info_block = ""
@@ -1148,12 +1132,13 @@ class HeartFChatting:
else: else:
chat_content_block = "当前没有观察到新的聊天内容。\n" chat_content_block = "当前没有观察到新的聊天内容。\n"
# 准备当前思维块 # 准备当前思维块 (修改以匹配模板)
current_mind_block = "" current_mind_block = ""
if current_mind: if current_mind:
current_mind_block = f"{current_mind}" # 模板中占位符是 {current_mind_block},它期望包含"你的内心想法:"的前缀
current_mind_block = f"你的内心想法:\n{current_mind}"
else: else:
current_mind_block = "[没有特别的想法]" current_mind_block = "你的内心想法:\n[没有特别的想法]"
# 准备循环信息块 (分析最近的活动循环) # 准备循环信息块 (分析最近的活动循环)
recent_active_cycles = [] recent_active_cycles = []
@@ -1193,23 +1178,40 @@ class HeartFChatting:
# 包装提示块,增加可读性,即使没有连续回复也给个标记 # 包装提示块,增加可读性,即使没有连续回复也给个标记
if cycle_info_block: if cycle_info_block:
# 模板中占位符是 {cycle_info_block},它期望包含"【近期回复历史】"的前缀
cycle_info_block = f"\n【近期回复历史】\n{cycle_info_block}\n" cycle_info_block = f"\n【近期回复历史】\n{cycle_info_block}\n"
else: else:
# 如果最近的活动循环不是文本回复,或者没有活动循环 # 如果最近的活动循环不是文本回复,或者没有活动循环
cycle_info_block = "\n【近期回复历史】\n(最近没有连续文本回复)\n" cycle_info_block = "\n【近期回复历史】\n(最近没有连续文本回复)\n"
individuality = Individuality.get_instance() individuality = Individuality.get_instance()
# 模板中占位符是 {prompt_personality}
prompt_personality = individuality.get_prompt(x_person=2, level=2) prompt_personality = individuality.get_prompt(x_person=2, level=2)
# 获取提示词模板并填充数据 # --- 构建可用动作描述 (用于填充模板中的 {action_options_text}) ---
prompt = (await global_prompt_manager.get_prompt_async("planner_prompt")).format( action_options_text = "当前你可以选择的行动有:\n"
action_keys = list(current_available_actions.keys())
for name in action_keys:
desc = current_available_actions[name]
action_options_text += f"- '{name}': {desc}\n"
# --- 选择一个示例动作键 (用于填充模板中的 {example_action}) ---
example_action_key = action_keys[0] if action_keys else "no_reply"
# --- 获取提示词模板 ---
planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt")
# --- 填充模板 ---
prompt = planner_prompt_template.format(
bot_name=global_config.BOT_NICKNAME, bot_name=global_config.BOT_NICKNAME,
prompt_personality=prompt_personality, prompt_personality=prompt_personality,
structured_info_block=structured_info_block, structured_info_block=structured_info_block,
chat_content_block=chat_content_block, chat_content_block=chat_content_block,
current_mind_block=current_mind_block, current_mind_block=current_mind_block,
replan=replan_prompt, replan="", # 暂时留空 replan 信息
cycle_info_block=cycle_info_block, cycle_info_block=cycle_info_block,
action_options_text=action_options_text, # 传入可用动作描述
example_action=example_action_key, # 传入示例动作键
) )
return prompt return prompt
@@ -1217,7 +1219,7 @@ class HeartFChatting:
except Exception as e: except Exception as e:
logger.error(f"{self.log_prefix}[Planner] 构建提示词时出错: {e}") logger.error(f"{self.log_prefix}[Planner] 构建提示词时出错: {e}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return "" return "[构建 Planner Prompt 时出错]" # 返回错误提示,避免空字符串
# --- 回复器 (Replier) 的定义 --- # # --- 回复器 (Replier) 的定义 --- #
async def _replier_work( async def _replier_work(
@@ -1258,7 +1260,7 @@ class HeartFChatting:
try: try:
with Timer("LLM生成", {}): # 内部计时器,可选保留 with Timer("LLM生成", {}): # 内部计时器,可选保留
content, reasoning_content, model_name = await self.model_normal.generate_response(prompt) content, reasoning_content, model_name = await self.model_normal.generate_response(prompt)
logger.info(f"{self.log_prefix}[Replier-{thinking_id}]\\nPrompt:\\n{prompt}\\n生成回复: {content}\\n") # logger.info(f"{self.log_prefix}[Replier-{thinking_id}]\\nPrompt:\\n{prompt}\\n生成回复: {content}\\n")
# 捕捉 LLM 输出信息 # 捕捉 LLM 输出信息
info_catcher.catch_after_llm_generated( info_catcher.catch_after_llm_generated(
prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=model_name prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=model_name

View File

@@ -47,17 +47,15 @@ def init_prompt():
"info_from_tools", "info_from_tools",
) )
# Planner提示词 - 优化版 # Planner提示词 - 修改为要求 JSON 输出
Prompt( Prompt(
"""你的名字是{bot_name},{prompt_personality},你现在正在一个群聊中。需要基于以下信息决定如何参与对话: """你的名字是{bot_name},{prompt_personality},你现在正在一个群聊中。需要基于以下信息决定如何参与对话:
{structured_info_block} {structured_info_block}
{chat_content_block} {chat_content_block}
你的内心想法:
{current_mind_block} {current_mind_block}
{replan}
{cycle_info_block} {cycle_info_block}
请综合分析聊天内容和你看到的新消息,参考内心想法,使用'decide_reply_action'工具做出决策。决策时请注意: 请综合分析聊天内容和你看到的新消息,参考内心想法,并根据以下原则和可用动作做出决策。
【回复原则】 【回复原则】
1. 不回复(no_reply)适用: 1. 不回复(no_reply)适用:
@@ -69,7 +67,7 @@ def init_prompt():
2. 文字回复(text_reply)适用: 2. 文字回复(text_reply)适用:
- 有实质性内容需要表达 - 有实质性内容需要表达
- 有人提到你,但你还没有回应他 - 有人提到你,但你还没有回应他
- 可以追加emoji_query表达情绪(格式:情绪描述,如"俏皮的调侃") - 可以追加emoji_query表达情绪(emoji_query填写表情包的适用场合也就是当前场合)
- 不要追加太多表情 - 不要追加太多表情
3. 纯表情回复(emoji_reply)适用: 3. 纯表情回复(emoji_reply)适用:
@@ -81,14 +79,34 @@ def init_prompt():
- 避免重复或评价自己的发言 - 避免重复或评价自己的发言
- 不要和自己聊天 - 不要和自己聊天
必须遵守 决策任务
- 遵守回复原则 {action_options_text}
- 必须调用工具并包含action和reasoning
- 你可以选择文字回复(text_reply),纯表情回复(emoji_reply),不回复(no_reply) 你必须从上面列出的可用行动中选择一个,并说明原因。
- 并不是所有选择都可用 你的决策必须以严格的 JSON 格式输出,且仅包含 JSON 内容,不要有任何其他文字或解释。
- 选择text_reply或emoji_reply时必须提供emoji_query JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query":
- 保持回复自然,符合日常聊天习惯""", {{
"planner_prompt", "action": "string", // 必须是上面提供的可用行动之一 (例如: '{example_action}')
"reasoning": "string", // 做出此决定的详细理由和思考过程,说明你如何应用了回复原则
"emoji_query": "string" // 可选。如果行动是 'emoji_reply',必须提供表情主题(填写表情包的适用场合);如果行动是 'text_reply' 且你想附带表情,也在此提供表情主题,否则留空字符串 ""。遵循回复原则,不要滥用。
}}
例如:
{{
"action": "text_reply",
"reasoning": "用户提到了我,且问题比较具体,适合用文本回复。考虑到内容,可以带上一个微笑表情。",
"emoji_query": "微笑"
}}
{{
"action": "no_reply",
"reasoning": "我已经连续回复了两次,而且这个话题我不太感兴趣,根据回复原则,选择不回复,等待其他人发言。",
"emoji_query": ""
}}
请输出你的决策 JSON
""", # 使用三引号避免内部引号问题
"planner_prompt", # 保持名称不变,替换内容
) )
Prompt( Prompt(
@@ -177,7 +195,7 @@ class PromptBuilder:
message_list_before_now = get_raw_msg_before_timestamp_with_chat( message_list_before_now = get_raw_msg_before_timestamp_with_chat(
chat_id=chat_stream.stream_id, chat_id=chat_stream.stream_id,
timestamp=time.time(), timestamp=time.time(),
limit=global_config.MAX_CONTEXT_SIZE, limit=global_config.observation_context_size,
) )
chat_talking_prompt = await build_readable_messages( chat_talking_prompt = await build_readable_messages(
@@ -246,6 +264,8 @@ class PromptBuilder:
sender_name=sender_name, sender_name=sender_name,
) )
logger.debug(f"focus_chat_prompt: \n{prompt}")
return prompt return prompt
async def _build_prompt_normal(self, chat_stream, message_txt: str, sender_name: str = "某人") -> tuple[str, str]: async def _build_prompt_normal(self, chat_stream, message_txt: str, sender_name: str = "某人") -> tuple[str, str]:
@@ -259,15 +279,15 @@ class PromptBuilder:
who_chat_in_group += get_recent_group_speaker( who_chat_in_group += get_recent_group_speaker(
chat_stream.stream_id, chat_stream.stream_id,
(chat_stream.user_info.platform, chat_stream.user_info.user_id), (chat_stream.user_info.platform, chat_stream.user_info.user_id),
limit=global_config.MAX_CONTEXT_SIZE, limit=global_config.observation_context_size,
) )
relation_prompt = "" relation_prompt = ""
for person in who_chat_in_group: for person in who_chat_in_group:
relation_prompt += await relationship_manager.build_relationship_info(person) relation_prompt += await relationship_manager.build_relationship_info(person)
print(f"relation_prompt: {relation_prompt}") # print(f"relation_prompt: {relation_prompt}")
print(f"relat11111111ion_prompt: {relation_prompt}") # print(f"relat11111111ion_prompt: {relation_prompt}")
# 心情 # 心情
mood_manager = MoodManager.get_instance() mood_manager = MoodManager.get_instance()
@@ -318,7 +338,7 @@ class PromptBuilder:
message_list_before_now = get_raw_msg_before_timestamp_with_chat( message_list_before_now = get_raw_msg_before_timestamp_with_chat(
chat_id=chat_stream.stream_id, chat_id=chat_stream.stream_id,
timestamp=time.time(), timestamp=time.time(),
limit=global_config.MAX_CONTEXT_SIZE, limit=global_config.observation_context_size,
) )
chat_talking_prompt = await build_readable_messages( chat_talking_prompt = await build_readable_messages(

View File

@@ -44,6 +44,8 @@ class NormalChat:
# 存储此实例的兴趣监控任务 # 存储此实例的兴趣监控任务
self.start_time = time.time() self.start_time = time.time()
self.last_speak_time = 0
self._chat_task: Optional[asyncio.Task] = None self._chat_task: Optional[asyncio.Task] = None
logger.info(f"[{self.stream_name}] NormalChat 实例初始化完成。") logger.info(f"[{self.stream_name}] NormalChat 实例初始化完成。")
@@ -119,6 +121,8 @@ class NormalChat:
await message_manager.add_message(message_set) await message_manager.add_message(message_set)
self.last_speak_time = time.time()
return first_bot_msg return first_bot_msg
# 改为实例方法 # 改为实例方法
@@ -354,7 +358,9 @@ class NormalChat:
processed_count = 0 processed_count = 0
# --- 修改迭代前创建要处理的ID列表副本防止迭代时修改 --- # --- 修改迭代前创建要处理的ID列表副本防止迭代时修改 ---
messages_to_process_initially = list(messages_to_reply) # 创建副本 messages_to_process_initially = list(messages_to_reply) # 创建副本
# --- 修改结束 --- # --- 新增:限制最多处理两条消息 ---
messages_to_process_initially = messages_to_process_initially[:2]
# --- 新增结束 ---
for item in messages_to_process_initially: # 使用副本迭代 for item in messages_to_process_initially: # 使用副本迭代
msg_id, (message, interest_value, is_mentioned) = item msg_id, (message, interest_value, is_mentioned) = item
# --- 修改:在处理前尝试 pop防止竞争 --- # --- 修改:在处理前尝试 pop防止竞争 ---
@@ -439,7 +445,7 @@ class NormalChat:
logger.error(f"[{self.stream_name}] 任务异常: {exc}") logger.error(f"[{self.stream_name}] 任务异常: {exc}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
except asyncio.CancelledError: except asyncio.CancelledError:
logger.info(f"[{self.stream_name}] 任务已取消") logger.debug(f"[{self.stream_name}] 任务已取消")
except Exception as e: except Exception as e:
logger.error(f"[{self.stream_name}] 回调处理错误: {e}") logger.error(f"[{self.stream_name}] 回调处理错误: {e}")
finally: finally:
@@ -452,12 +458,12 @@ class NormalChat:
"""停止当前实例的兴趣监控任务。""" """停止当前实例的兴趣监控任务。"""
if self._chat_task and not self._chat_task.done(): if self._chat_task and not self._chat_task.done():
task = self._chat_task task = self._chat_task
logger.info(f"[{self.stream_name}] 尝试取消聊天任务。") logger.debug(f"[{self.stream_name}] 尝试取消normal聊天任务。")
task.cancel() task.cancel()
try: try:
await task # 等待任务响应取消 await task # 等待任务响应取消
except asyncio.CancelledError: except asyncio.CancelledError:
logger.info(f"[{self.stream_name}] 聊天任务已成功取消") logger.info(f"[{self.stream_name}] 结束一般聊天模式")
except Exception as e: except Exception as e:
# 回调函数 _handle_task_completion 会处理异常日志 # 回调函数 _handle_task_completion 会处理异常日志
logger.warning(f"[{self.stream_name}] 等待监控任务取消时捕获到异常 (可能已在回调中记录): {e}") logger.warning(f"[{self.stream_name}] 等待监控任务取消时捕获到异常 (可能已在回调中记录): {e}")

View File

@@ -29,7 +29,7 @@ class NormalChatGenerator:
) )
self.model_sum = LLMRequest( self.model_sum = LLMRequest(
model=global_config.llm_summary_by_topic, temperature=0.7, max_tokens=3000, request_type="relation" model=global_config.llm_summary, temperature=0.7, max_tokens=3000, request_type="relation"
) )
self.current_model_type = "r1" # 默认使用 R1 self.current_model_type = "r1" # 默认使用 R1
self.current_model_name = "unknown model" self.current_model_name = "unknown model"
@@ -82,12 +82,14 @@ class NormalChatGenerator:
sender_name=sender_name, sender_name=sender_name,
chat_stream=message.chat_stream, chat_stream=message.chat_stream,
) )
logger.info(f"构建prompt时间: {t_build_prompt.human_readable}") logger.debug(f"构建prompt时间: {t_build_prompt.human_readable}")
try: try:
content, reasoning_content, self.current_model_name = await model.generate_response(prompt) content, reasoning_content, self.current_model_name = await model.generate_response(prompt)
logger.info(f"prompt:{prompt}\n生成回复:{content}") logger.debug(f"prompt:{prompt}\n生成回复:{content}")
logger.info(f"{message.processed_plain_text} 的回复:{content}")
info_catcher.catch_after_llm_generated( info_catcher.catch_after_llm_generated(
prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=self.current_model_name prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=self.current_model_name

View File

@@ -11,6 +11,9 @@ from .lpmmconfig import global_config
from .utils.dyn_topk import dyn_select_top_k from .utils.dyn_topk import dyn_select_top_k
MAX_KNOWLEDGE_LENGTH = 10000 # 最大知识长度
class QAManager: class QAManager:
def __init__( def __init__(
self, self,
@@ -112,8 +115,10 @@ class QAManager:
for res in query_res for res in query_res
] ]
found_knowledge = "\n".join( found_knowledge = "\n".join(
[f"{i + 1}条知识:{k[1]}\n 该条知识对于问题的相关性:{k[0]}" for i, k in enumerate(knowledge)] [f"{i + 1}条知识:{k[0]}\n 该条知识对于问题的相关性:{k[1]}" for i, k in enumerate(knowledge)]
) )
if len(found_knowledge) > MAX_KNOWLEDGE_LENGTH:
found_knowledge = found_knowledge[:MAX_KNOWLEDGE_LENGTH] + "\n"
return found_knowledge return found_knowledge
else: else:
logger.info("LPMM知识库并未初始化使用旧版数据库进行检索") logger.info("LPMM知识库并未初始化使用旧版数据库进行检索")

View File

@@ -189,7 +189,7 @@ class Hippocampus:
def __init__(self): def __init__(self):
self.memory_graph = MemoryGraph() self.memory_graph = MemoryGraph()
self.llm_topic_judge = None self.llm_topic_judge = None
self.llm_summary_by_topic = None self.llm_summary = None
self.entorhinal_cortex = None self.entorhinal_cortex = None
self.parahippocampal_gyrus = None self.parahippocampal_gyrus = None
self.config = None self.config = None
@@ -203,7 +203,7 @@ class Hippocampus:
# 从数据库加载记忆图 # 从数据库加载记忆图
self.entorhinal_cortex.sync_memory_from_db() self.entorhinal_cortex.sync_memory_from_db()
self.llm_topic_judge = LLMRequest(self.config.llm_topic_judge, request_type="memory") self.llm_topic_judge = LLMRequest(self.config.llm_topic_judge, request_type="memory")
self.llm_summary_by_topic = LLMRequest(self.config.llm_summary_by_topic, request_type="memory") self.llm_summary = LLMRequest(self.config.llm_summary, request_type="memory")
def get_all_node_names(self) -> list: def get_all_node_names(self) -> list:
"""获取记忆图中所有节点的名字列表""" """获取记忆图中所有节点的名字列表"""
@@ -1169,7 +1169,7 @@ class ParahippocampalGyrus:
# 调用修改后的 topic_what不再需要 time_info # 调用修改后的 topic_what不再需要 time_info
topic_what_prompt = self.hippocampus.topic_what(input_text, topic) topic_what_prompt = self.hippocampus.topic_what(input_text, topic)
try: try:
task = self.hippocampus.llm_summary_by_topic.generate_response_async(topic_what_prompt) task = self.hippocampus.llm_summary.generate_response_async(topic_what_prompt)
tasks.append((topic.strip(), task)) tasks.append((topic.strip(), task))
except Exception as e: except Exception as e:
logger.error(f"生成话题 '{topic}' 的摘要时发生错误: {e}") logger.error(f"生成话题 '{topic}' 的摘要时发生错误: {e}")

View File

@@ -24,7 +24,7 @@ class MemoryConfig:
consolidate_memory_interval: int # 记忆整合间隔 consolidate_memory_interval: int # 记忆整合间隔
llm_topic_judge: str # 话题判断模型 llm_topic_judge: str # 话题判断模型
llm_summary_by_topic: str # 话题总结模型 llm_summary: str # 话题总结模型
@classmethod @classmethod
def from_global_config(cls, global_config): def from_global_config(cls, global_config):
@@ -44,7 +44,5 @@ class MemoryConfig:
consolidate_memory_percentage=getattr(global_config, "consolidate_memory_percentage", 0.01), consolidate_memory_percentage=getattr(global_config, "consolidate_memory_percentage", 0.01),
consolidate_memory_interval=getattr(global_config, "consolidate_memory_interval", 1000), consolidate_memory_interval=getattr(global_config, "consolidate_memory_interval", 1000),
llm_topic_judge=getattr(global_config, "llm_topic_judge", "default_judge_model"), # 添加默认模型名 llm_topic_judge=getattr(global_config, "llm_topic_judge", "default_judge_model"), # 添加默认模型名
llm_summary_by_topic=getattr( llm_summary=getattr(global_config, "llm_summary", "default_summary_model"), # 添加默认模型名
global_config, "llm_summary_by_topic", "default_summary_model"
), # 添加默认模型名
) )

View File

@@ -632,7 +632,7 @@ class LLMRequest:
**params_copy, **params_copy,
} }
if "max_tokens" not in payload and "max_completion_tokens" not in payload: if "max_tokens" not in payload and "max_completion_tokens" not in payload:
payload["max_tokens"] = global_config.max_response_length payload["max_tokens"] = global_config.model_max_output_length
# 如果 payload 中依然存在 max_tokens 且需要转换,在这里进行再次检查 # 如果 payload 中依然存在 max_tokens 且需要转换,在这里进行再次检查
if self.model_name.lower() in self.MODELS_NEEDING_TRANSFORMATION and "max_tokens" in payload: if self.model_name.lower() in self.MODELS_NEEDING_TRANSFORMATION and "max_tokens" in payload:
payload["max_completion_tokens"] = payload.pop("max_tokens") payload["max_completion_tokens"] = payload.pop("max_tokens")

View File

@@ -282,10 +282,10 @@ class RelationshipManager:
if is_id: if is_id:
person_id = person person_id = person
else: else:
print(f"person: {person}") # print(f"person: {person}")
person_id = person_info_manager.get_person_id(person[0], person[1]) person_id = person_info_manager.get_person_id(person[0], person[1])
person_name = await person_info_manager.get_value(person_id, "person_name") person_name = await person_info_manager.get_value(person_id, "person_name")
print(f"person_name: {person_name}") # print(f"person_name: {person_name}")
relationship_value = await person_info_manager.get_value(person_id, "relationship_value") relationship_value = await person_info_manager.get_value(person_id, "relationship_value")
level_num = self.calculate_level_num(relationship_value) level_num = self.calculate_level_num(relationship_value)

View File

@@ -8,13 +8,12 @@ from typing import List
class InfoCatcher: class InfoCatcher:
def __init__(self): def __init__(self):
self.chat_history = [] # 聊天历史,长度为三倍使用的上下文 self.chat_history = [] # 聊天历史,长度为三倍使用的上下文喵~
self.context_length = global_config.MAX_CONTEXT_SIZE self.context_length = global_config.observation_context_size
self.chat_history_in_thinking = [] # 思考期间的聊天内容 self.chat_history_in_thinking = [] # 思考期间的聊天内容喵~
self.chat_history_after_response = [] # 回复后的聊天内容,长度为一倍上下文 self.chat_history_after_response = [] # 回复后的聊天内容,长度为一倍上下文喵~
self.chat_id = "" self.chat_id = ""
self.response_mode = global_config.response_mode
self.trigger_response_text = "" self.trigger_response_text = ""
self.response_text = "" self.response_text = ""
@@ -36,10 +35,10 @@ class InfoCatcher:
"model": "", "model": "",
} }
# 使用字典来存储 reasoning 模式的数据 # 使用字典来存储 reasoning 模式的数据喵~
self.reasoning_data = {"thinking_log": "", "prompt": "", "response": "", "model": ""} self.reasoning_data = {"thinking_log": "", "prompt": "", "response": "", "model": ""}
# 耗时 # 耗时喵~
self.timing_results = { self.timing_results = {
"interested_rate_time": 0, "interested_rate_time": 0,
"sub_heartflow_observe_time": 0, "sub_heartflow_observe_time": 0,
@@ -73,15 +72,25 @@ class InfoCatcher:
self.heartflow_data["sub_heartflow_now"] = current_mind self.heartflow_data["sub_heartflow_now"] = current_mind
def catch_after_llm_generated(self, prompt: str, response: str, reasoning_content: str = "", model_name: str = ""): def catch_after_llm_generated(self, prompt: str, response: str, reasoning_content: str = "", model_name: str = ""):
if self.response_mode == "heart_flow": # if self.response_mode == "heart_flow": # 条件判断不需要了喵~
self.heartflow_data["prompt"] = prompt # self.heartflow_data["prompt"] = prompt
self.heartflow_data["response"] = response # self.heartflow_data["response"] = response
self.heartflow_data["model"] = model_name # self.heartflow_data["model"] = model_name
elif self.response_mode == "reasoning": # elif self.response_mode == "reasoning": # 条件判断不需要了喵~
self.reasoning_data["thinking_log"] = reasoning_content # self.reasoning_data["thinking_log"] = reasoning_content
self.reasoning_data["prompt"] = prompt # self.reasoning_data["prompt"] = prompt
self.reasoning_data["response"] = response # self.reasoning_data["response"] = response
self.reasoning_data["model"] = model_name # self.reasoning_data["model"] = model_name
# 直接记录信息喵~
self.reasoning_data["thinking_log"] = reasoning_content
self.reasoning_data["prompt"] = prompt
self.reasoning_data["response"] = response
self.reasoning_data["model"] = model_name
# 如果 heartflow 数据也需要通用字段,可以取消下面的注释喵~
# self.heartflow_data["prompt"] = prompt
# self.heartflow_data["response"] = response
# self.heartflow_data["model"] = model_name
self.response_text = response self.response_text = response
@@ -172,13 +181,13 @@ class InfoCatcher:
} }
def done_catch(self): def done_catch(self):
"""将收集到的信息存储到数据库的 thinking_log 集合中""" """将收集到的信息存储到数据库的 thinking_log 集合中喵~"""
try: try:
# 将消息对象转换为可序列化的字典 # 将消息对象转换为可序列化的字典喵~
thinking_log_data = { thinking_log_data = {
"chat_id": self.chat_id, "chat_id": self.chat_id,
"response_mode": self.response_mode, # "response_mode": self.response_mode, # 这个也删掉喵~
"trigger_text": self.trigger_response_text, "trigger_text": self.trigger_response_text,
"response_text": self.response_text, "response_text": self.response_text,
"trigger_info": { "trigger_info": {
@@ -195,18 +204,20 @@ class InfoCatcher:
"chat_history_after_response": self.message_list_to_dict(self.chat_history_after_response), "chat_history_after_response": self.message_list_to_dict(self.chat_history_after_response),
} }
# 根据不同的响应模式添加相应的数据 # 根据不同的响应模式添加相应的数据喵~ # 现在直接都加上去好了喵~
if self.response_mode == "heart_flow": # if self.response_mode == "heart_flow":
thinking_log_data["mode_specific_data"] = self.heartflow_data # thinking_log_data["mode_specific_data"] = self.heartflow_data
elif self.response_mode == "reasoning": # elif self.response_mode == "reasoning":
thinking_log_data["mode_specific_data"] = self.reasoning_data # thinking_log_data["mode_specific_data"] = self.reasoning_data
thinking_log_data["heartflow_data"] = self.heartflow_data
thinking_log_data["reasoning_data"] = self.reasoning_data
# 将数据插入到 thinking_log 集合中 # 将数据插入到 thinking_log 集合中喵~
db.thinking_log.insert_one(thinking_log_data) db.thinking_log.insert_one(thinking_log_data)
return True return True
except Exception as e: except Exception as e:
print(f"存储思考日志时出错: {str(e)}") print(f"存储思考日志时出错: {str(e)} 喵~")
print(traceback.format_exc()) print(traceback.format_exc())
return False return False

View File

@@ -1,6 +1,7 @@
import json import json
import logging import logging
from typing import Any, Dict, TypeVar, List, Union, Tuple from typing import Any, Dict, TypeVar, List, Union, Tuple
import ast
# 定义类型变量用于泛型类型提示 # 定义类型变量用于泛型类型提示
T = TypeVar("T") T = TypeVar("T")
@@ -12,6 +13,7 @@ logger = logging.getLogger("json_utils")
def safe_json_loads(json_str: str, default_value: T = None) -> Union[Any, T]: def safe_json_loads(json_str: str, default_value: T = None) -> Union[Any, T]:
""" """
安全地解析JSON字符串出错时返回默认值 安全地解析JSON字符串出错时返回默认值
现在尝试处理单引号和标准JSON
参数: 参数:
json_str: 要解析的JSON字符串 json_str: 要解析的JSON字符串
@@ -20,16 +22,34 @@ def safe_json_loads(json_str: str, default_value: T = None) -> Union[Any, T]:
返回: 返回:
解析后的Python对象或在解析失败时返回default_value 解析后的Python对象或在解析失败时返回default_value
""" """
if not json_str: if not json_str or not isinstance(json_str, str):
logger.warning(f"safe_json_loads 接收到非字符串输入: {type(json_str)}, 值: {json_str}")
return default_value return default_value
try: try:
# 尝试标准的 JSON 解析
return json.loads(json_str) return json.loads(json_str)
except json.JSONDecodeError as e: except json.JSONDecodeError:
logger.error(f"JSON解析失败: {e}, JSON字符串: {json_str[:100]}...") # 如果标准解析失败,尝试将单引号替换为双引号再解析
return default_value # (注意:这种替换可能不安全,如果字符串内容本身包含引号)
# 更安全的方式是用 ast.literal_eval
try:
# logger.debug(f"标准JSON解析失败尝试用 ast.literal_eval 解析: {json_str[:100]}...")
result = ast.literal_eval(json_str)
# 确保结果是字典(因为我们通常期望参数是字典)
if isinstance(result, dict):
return result
else:
logger.warning(f"ast.literal_eval 解析成功但结果不是字典: {type(result)}, 内容: {result}")
return default_value
except (ValueError, SyntaxError, MemoryError, RecursionError) as ast_e:
logger.error(f"使用 ast.literal_eval 解析失败: {ast_e}, 字符串: {json_str[:100]}...")
return default_value
except Exception as e:
logger.error(f"使用 ast.literal_eval 解析时发生意外错误: {e}, 字符串: {json_str[:100]}...")
return default_value
except Exception as e: except Exception as e:
logger.error(f"JSON解析过程中发生意外错误: {e}") logger.error(f"JSON解析过程中发生意外错误: {e}, 字符串: {json_str[:100]}...")
return default_value return default_value
@@ -177,25 +197,27 @@ def process_llm_tool_calls(
if "name" not in func_details or not isinstance(func_details.get("name"), str): if "name" not in func_details or not isinstance(func_details.get("name"), str):
logger.warning(f"{log_prefix}工具调用[{i}]的'function'字段缺少'name'或类型不正确: {func_details}") logger.warning(f"{log_prefix}工具调用[{i}]的'function'字段缺少'name'或类型不正确: {func_details}")
continue continue
if "arguments" not in func_details or not isinstance(
func_details.get("arguments"), str # 验证参数 'arguments'
): # 参数是字符串形式的JSON args_value = func_details.get("arguments")
logger.warning(f"{log_prefix}工具调用[{i}]的'function'字段缺少'arguments'或类型不正确: {func_details}")
# 1. 检查 arguments 是否存在且是字符串
if args_value is None or not isinstance(args_value, str):
logger.warning(f"{log_prefix}工具调用[{i}]的'function'字段缺少'arguments'字符串: {func_details}")
continue continue
# 可选尝试解析参数JSON确保其有效 # 2. 尝试安全地解析 arguments 字符串
args_str = func_details["arguments"] parsed_args = safe_json_loads(args_value, None)
try:
json.loads(args_str) # 尝试解析,但不存储结果 # 3. 检查解析结果是否为字典
except json.JSONDecodeError as e: if parsed_args is None or not isinstance(parsed_args, dict):
logger.warning( logger.warning(
f"{log_prefix}工具调用[{i}]的'arguments'不是有效的JSON字符串: {e}, 内容: {args_str[:100]}..." f"{log_prefix}工具调用[{i}]的'arguments'无法解析为有效的JSON字典, "
f"原始字符串: {args_value[:100]}..., 解析结果类型: {type(parsed_args).__name__}"
) )
continue continue
except Exception as e:
logger.warning(f"{log_prefix}解析工具调用[{i}]的'arguments'时发生意外错误: {e}, 内容: {args_str[:100]}...")
continue
# 如果检查通过,将原始的 tool_call 加入有效列表
valid_tool_calls.append(tool_call) valid_tool_calls.append(tool_call)
if not valid_tool_calls and tool_calls: # 如果原始列表不为空,但验证后为空 if not valid_tool_calls and tool_calls: # 如果原始列表不为空,但验证后为空

View File

@@ -64,6 +64,9 @@ class ClassicalWillingManager(BaseWillingManager):
self.chat_reply_willing[chat_id] = max(0, current_willing - 1.8) self.chat_reply_willing[chat_id] = max(0, current_willing - 1.8)
async def after_generate_reply_handle(self, message_id): async def after_generate_reply_handle(self, message_id):
if message_id not in self.ongoing_messages:
return
chat_id = self.ongoing_messages[message_id].chat_id chat_id = self.ongoing_messages[message_id].chat_id
current_willing = self.chat_reply_willing.get(chat_id, 0) current_willing = self.chat_reply_willing.get(chat_id, 0)
if current_willing < 1: if current_willing < 1:

View File

@@ -77,7 +77,7 @@ class BaseWillingManager(ABC):
if not issubclass(manager_class, cls): if not issubclass(manager_class, cls):
raise TypeError(f"Manager class {manager_class.__name__} is not a subclass of {cls.__name__}") raise TypeError(f"Manager class {manager_class.__name__} is not a subclass of {cls.__name__}")
else: else:
logger.info(f"成功载入willing模式:{manager_type}") logger.info(f"普通回复模式:{manager_type}")
return manager_class() return manager_class()
except (ImportError, AttributeError, TypeError) as e: except (ImportError, AttributeError, TypeError) as e:
module = importlib.import_module(".mode_classical", __package__) module = importlib.import_module(".mode_classical", __package__)
@@ -110,7 +110,7 @@ class BaseWillingManager(ABC):
def delete(self, message_id: str): def delete(self, message_id: str):
del_message = self.ongoing_messages.pop(message_id, None) del_message = self.ongoing_messages.pop(message_id, None)
if not del_message: if not del_message:
logger.debug(f"删除异常,当前消息{message_id}不存在") logger.debug(f"尝试删除不存在的消息 ID: {message_id},可能已被其他流程处理,喵~")
@abstractmethod @abstractmethod
async def async_task_starter(self) -> None: async def async_task_starter(self) -> None:

View File

@@ -1,5 +1,5 @@
[inner] [inner]
version = "1.5.1" version = "1.6.0"
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
#如果你想要修改配置文件请在修改后将version的值进行变更 #如果你想要修改配置文件请在修改后将version的值进行变更
@@ -65,33 +65,14 @@ time_zone = "Asia/Shanghai" # 给你的机器人设置时区,可以解决运
[platforms] # 必填项目,填写每个平台适配器提供的链接 [platforms] # 必填项目,填写每个平台适配器提供的链接
nonebot-qq="http://127.0.0.1:18002/api/message" nonebot-qq="http://127.0.0.1:18002/api/message"
[response] #群聊的回复策略 [chat] #麦麦的聊天通用设置
#一般回复参数 allow_focus_mode = true # 是否允许专注聊天状态
model_reasoning_probability = 0.7 # 麦麦回答时选择推理模型 模型的概率 # 是否启用heart_flowC(HFC)模式
model_normal_probability = 0.3 # 麦麦回答时选择一般模型 模型的概率
[heartflow]
allow_focus_mode = true # 是否允许进入FOCUSED状态
# 是否启用heart_flowC(心流聊天,HFC)模式
# 启用后麦麦会自主选择进入heart_flowC模式(持续一段时间进行主动的观察和回复并给出回复比较消耗token # 启用后麦麦会自主选择进入heart_flowC模式(持续一段时间进行主动的观察和回复并给出回复比较消耗token
reply_trigger_threshold = 3.0 # 心流聊天触发阈值,越低越容易进入心流聊天 base_normal_chat_num = 3 # 最多允许多少个群进行普通聊天
probability_decay_factor_per_second = 0.2 # 概率衰减因子,越大衰减越快,越高越容易退出心流聊天 base_focused_chat_num = 2 # 最多允许多少个群进行专注聊天
default_decay_rate_per_second = 0.98 # 默认衰减率,越大衰减越快,越高越难进入心流聊天
observation_context_size = 15 # 观察到的最长上下文大小,建议15太短太长都会导致脑袋尖尖
sub_heart_flow_stop_time = 500 # 子心流停止时间,超过这个时间没有回复,子心流会停止,间隔 单位秒
observation_context_size = 20 # 心流观察到的最长上下文大小,超过这个值的上下文会被压缩
compressed_length = 5 # 不能大于observation_context_size,心流上下文压缩的最短压缩长度超过心流观察到的上下文长度会压缩最短压缩长度为5
compress_length_limit = 5 #最多压缩份数,超过该数值的压缩上下文会被删除
[message]
max_context_size = 12 # 麦麦回复时获得的上文数量建议12太短太长都会导致脑袋尖尖
emoji_chance = 0.2 # 麦麦一般回复时使用表情包的概率设置为1让麦麦自己决定发不发
thinking_timeout = 100 # 麦麦最长思考时间超过这个时间的思考会放弃往往是api反应太慢
max_response_length = 256 # 麦麦单次回答的最大token数
message_buffer = true # 启用消息缓冲器?启用此项以解决消息的拆分问题,但会使麦麦的回复延迟 message_buffer = true # 启用消息缓冲器?启用此项以解决消息的拆分问题,但会使麦麦的回复延迟
# 以下是消息过滤,可以根据规则过滤特定消息,将不会读取这些消息 # 以下是消息过滤,可以根据规则过滤特定消息,将不会读取这些消息
@@ -106,7 +87,14 @@ ban_msgs_regex = [
# "\\[CQ:at,qq=\\d+\\]" # 匹配@ # "\\[CQ:at,qq=\\d+\\]" # 匹配@
] ]
[willing] # 一般回复模式的回复意愿设置 [normal_chat] #普通聊天
#一般回复参数
model_reasoning_probability = 0.7 # 麦麦回答时选择推理模型 模型的概率
model_normal_probability = 0.3 # 麦麦回答时选择一般模型 模型的概率
emoji_chance = 0.2 # 麦麦一般回复时使用表情包的概率设置为1让麦麦自己决定发不发
thinking_timeout = 100 # 麦麦最长思考时间超过这个时间的思考会放弃往往是api反应太慢
willing_mode = "classical" # 回复意愿模式 —— 经典模式classical动态模式dynamicmxp模式mxp自定义模式custom需要你自己实现 willing_mode = "classical" # 回复意愿模式 —— 经典模式classical动态模式dynamicmxp模式mxp自定义模式custom需要你自己实现
response_willing_amplifier = 1 # 麦麦回复意愿放大系数一般为1 response_willing_amplifier = 1 # 麦麦回复意愿放大系数一般为1
response_interested_rate_amplifier = 1 # 麦麦回复兴趣度放大系数,听到记忆里的内容时放大系数 response_interested_rate_amplifier = 1 # 麦麦回复兴趣度放大系数,听到记忆里的内容时放大系数
@@ -115,6 +103,16 @@ emoji_response_penalty = 0 # 表情包回复惩罚系数设为0为不回复
mentioned_bot_inevitable_reply = false # 提及 bot 必然回复 mentioned_bot_inevitable_reply = false # 提及 bot 必然回复
at_bot_inevitable_reply = false # @bot 必然回复 at_bot_inevitable_reply = false # @bot 必然回复
[focus_chat] #专注聊天
reply_trigger_threshold = 3.6 # 专注聊天触发阈值,越低越容易进入专注聊天
default_decay_rate_per_second = 0.95 # 默认衰减率,越大衰减越快,越高越难进入专注聊天
consecutive_no_reply_threshold = 3 # 连续不回复的阈值,越低越容易结束专注聊天
# 以下选项暂时无效
compressed_length = 5 # 不能大于observation_context_size,心流上下文压缩的最短压缩长度超过心流观察到的上下文长度会压缩最短压缩长度为5
compress_length_limit = 5 #最多压缩份数,超过该数值的压缩上下文会被删除
[emoji] [emoji]
max_emoji_num = 40 # 表情包最大数量 max_emoji_num = 40 # 表情包最大数量
max_reach_deletion = true # 开启则在达到最大数量时删除表情包,关闭则达到最大数量时不删除,只是不会继续收集表情包 max_reach_deletion = true # 开启则在达到最大数量时删除表情包,关闭则达到最大数量时不删除,只是不会继续收集表情包
@@ -181,6 +179,8 @@ response_max_length = 256 # 回复允许的最大长度
response_max_sentence_num = 4 # 回复允许的最大句子数 response_max_sentence_num = 4 # 回复允许的最大句子数
enable_kaomoji_protection = false # 是否启用颜文字保护 enable_kaomoji_protection = false # 是否启用颜文字保护
model_max_output_length = 256 # 模型单次返回的最大token数
[remote] #发送统计信息,主要是看全球有多少只麦麦 [remote] #发送统计信息,主要是看全球有多少只麦麦
enable = true enable = true
@@ -197,55 +197,44 @@ pfc_chatting = false # 是否启用PFC聊天该功能仅作用于私聊
# stream = <true|false> : 用于指定模型是否是使用流式输出 # stream = <true|false> : 用于指定模型是否是使用流式输出
# 如果不指定,则该项是 False # 如果不指定,则该项是 False
[model.llm_reasoning] #只在回复模式为reasoning时启用 #这个模型必须是推理模型
[model.llm_reasoning] # 一般聊天模式的推理回复模型
name = "Pro/deepseek-ai/DeepSeek-R1" name = "Pro/deepseek-ai/DeepSeek-R1"
# name = "Qwen/QwQ-32B"
provider = "SILICONFLOW" provider = "SILICONFLOW"
pri_in = 4 #模型的输入价格(非必填,可以记录消耗) pri_in = 1.0 #模型的输入价格(非必填,可以记录消耗)
pri_out = 16 #模型的输出价格(非必填,可以记录消耗) pri_out = 4.0 #模型的输出价格(非必填,可以记录消耗)
#非推理模型 [model.llm_normal] #V3 回复模型 专注和一般聊天模式共用的回复模型
[model.llm_normal] #V3 回复模型1 主要回复模型默认temp 0.2 如果你使用的是老V3或者其他模型请自己修改temp参数
name = "Pro/deepseek-ai/DeepSeek-V3" name = "Pro/deepseek-ai/DeepSeek-V3"
provider = "SILICONFLOW" provider = "SILICONFLOW"
pri_in = 2 #模型的输入价格(非必填,可以记录消耗) pri_in = 2 #模型的输入价格(非必填,可以记录消耗)
pri_out = 8 #模型的输出价格(非必填,可以记录消耗) pri_out = 8 #模型的输出价格(非必填,可以记录消耗)
#默认temp 0.2 如果你使用的是老V3或者其他模型请自己修改temp参数
temp = 0.2 #模型的温度新V3建议0.1-0.3 temp = 0.2 #模型的温度新V3建议0.1-0.3
[model.llm_emotion_judge] #表情包判断 [model.llm_topic_judge] #主题判断模型建议使用qwen2.5 7b
name = "Qwen/Qwen2.5-14B-Instruct"
provider = "SILICONFLOW"
pri_in = 0.7
pri_out = 0.7
[model.llm_topic_judge] #记忆主题判断建议使用qwen2.5 7b
name = "Pro/Qwen/Qwen2.5-7B-Instruct" name = "Pro/Qwen/Qwen2.5-7B-Instruct"
provider = "SILICONFLOW" provider = "SILICONFLOW"
pri_in = 0 pri_in = 0.35
pri_out = 0 pri_out = 0.35
[model.llm_summary_by_topic] #概括模型建议使用qwen2.5 32b 及以上 [model.llm_summary] #概括模型建议使用qwen2.5 32b 及以上
name = "Qwen/Qwen2.5-32B-Instruct" name = "Qwen/Qwen2.5-32B-Instruct"
provider = "SILICONFLOW" provider = "SILICONFLOW"
pri_in = 1.26 pri_in = 1.26
pri_out = 1.26 pri_out = 1.26
[model.llm_tool_use] #工具调用模型需要使用支持工具调用的模型建议使用qwen2.5 32b [model.vlm] # 图像识别模型
name = "Qwen/Qwen2.5-32B-Instruct"
provider = "SILICONFLOW"
pri_in = 1.26
pri_out = 1.26
# 识图模型
[model.vlm] #图像识别
name = "Pro/Qwen/Qwen2.5-VL-7B-Instruct" name = "Pro/Qwen/Qwen2.5-VL-7B-Instruct"
provider = "SILICONFLOW" provider = "SILICONFLOW"
pri_in = 0.35 pri_in = 0.35
pri_out = 0.35 pri_out = 0.35
[model.llm_heartflow] # 用于控制麦麦是否参与聊天的模型
name = "Qwen/Qwen2.5-32B-Instruct"
provider = "SILICONFLOW"
pri_in = 1.26
pri_out = 1.26
[model.llm_observation] #观察模型,压缩聊天内容,建议用免费的 [model.llm_observation] #观察模型,压缩聊天内容,建议用免费的
# name = "Pro/Qwen/Qwen2.5-7B-Instruct" # name = "Pro/Qwen/Qwen2.5-7B-Instruct"
@@ -254,19 +243,18 @@ provider = "SILICONFLOW"
pri_in = 0 pri_in = 0
pri_out = 0 pri_out = 0
[model.llm_sub_heartflow] #心流:认真水群时,生成麦麦的内心想法 [model.llm_sub_heartflow] #心流:认真水群时,生成麦麦的内心想法,必须使用具有工具调用能力的模型
name = "Qwen/Qwen2.5-72B-Instruct" name = "Pro/deepseek-ai/DeepSeek-V3"
provider = "SILICONFLOW" provider = "SILICONFLOW"
pri_in = 4.13 pri_in = 2
pri_out = 4.13 pri_out = 8
temp = 0.7 #模型的温度新V3建议0.1-0.3 temp = 0.3 #模型的温度新V3建议0.1-0.3
[model.llm_plan] #决策:认真水群时,负责决定麦麦该做什么
[model.llm_plan] #决策模型:认真水群时,负责决定麦麦该做什么 name = "Pro/deepseek-ai/DeepSeek-V3"
name = "Qwen/Qwen2.5-32B-Instruct"
provider = "SILICONFLOW" provider = "SILICONFLOW"
pri_in = 1.26 pri_in = 2
pri_out = 1.26 pri_out = 8
#嵌入模型 #嵌入模型
@@ -303,11 +291,13 @@ pri_in = 2
pri_out = 8 pri_out = 8
#模型暂时没有使用!! #以下模型暂时没有使用!!
#模型暂时没有使用!! #以下模型暂时没有使用!!
#模型暂时没有使用!! #以下模型暂时没有使用!!
[model.llm_heartflow] #心流 #以下模型暂时没有使用!!
# name = "Pro/Qwen/Qwen2.5-7B-Instruct" #以下模型暂时没有使用!!
[model.llm_tool_use] #工具调用模型需要使用支持工具调用的模型建议使用qwen2.5 32b
name = "Qwen/Qwen2.5-32B-Instruct" name = "Qwen/Qwen2.5-32B-Instruct"
provider = "SILICONFLOW" provider = "SILICONFLOW"
pri_in = 1.26 pri_in = 1.26

View File

@@ -0,0 +1,26 @@
@echo off
CHCP 65001 > nul
setlocal enabledelayedexpansion
REM 查找venv虚拟环境
set "venv_path=%~dp0venv\Scripts\activate.bat"
if not exist "%venv_path%" (
echo 错误: 未找到虚拟环境请确保venv目录存在
pause
exit /b 1
)
REM 激活虚拟环境
call "%venv_path%"
if %ERRORLEVEL% neq 0 (
echo 错误: 虚拟环境激活失败
pause
exit /b 1
)
echo 虚拟环境已激活,正在启动 GUI...
REM 运行 Python 脚本
python scripts/interest_monitor_gui.py
pause