diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml
index 76636d74..605d838c 100644
--- a/.github/workflows/docker-image.yml
+++ b/.github/workflows/docker-image.yml
@@ -24,6 +24,9 @@ jobs:
- name: Clone maim_message
run: git clone https://github.com/MaiM-with-u/maim_message maim_message
+ - name: Clone lpmm
+ run: git clone https://github.com/MaiM-with-u/MaiMBot-LPMM.git MaiMBot-LPMM
+
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
diff --git a/Dockerfile b/Dockerfile
index 07471152..23165a23 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,15 +6,22 @@ WORKDIR /MaiMBot
# 复制依赖列表
COPY requirements.txt .
-# 同级目录下需要有 maim_message
-COPY maim_message /maim_message
+# 同级目录下需要有 maim_message MaiMBot-LPMM
+#COPY maim_message /maim_message
+COPY MaiMBot-LPMM /MaiMBot-LPMM
# 编译器
-RUN apt-get update && apt-get install -y g++
+RUN apt-get update && apt-get install -y build-essential
+
+# lpmm编译安装
+RUN cd /MaiMBot-LPMM && uv pip install --system -r requirements.txt
+RUN uv pip install --system Cython py-cpuinfo setuptools
+RUN cd /MaiMBot-LPMM/lib/quick_algo && python build_lib.py --cleanup --cythonize --install
+
# 安装依赖
RUN uv pip install --system --upgrade pip
-RUN uv pip install --system -e /maim_message
+#RUN uv pip install --system -e /maim_message
RUN uv pip install --system -r requirements.txt
# 复制项目代码
diff --git a/README.md b/README.md
index 7eca2260..df5c1c94 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
-
+
diff --git a/interest_monitor_gui.py b/interest_monitor_gui.py
index 28c5ecc1..245a0ae9 100644
--- a/interest_monitor_gui.py
+++ b/interest_monitor_gui.py
@@ -246,7 +246,9 @@ class InterestMonitorApp:
self.stream_sub_minds[stream_id] = subflow_entry.get("sub_mind", "N/A")
self.stream_chat_states[stream_id] = subflow_entry.get("sub_chat_state", "N/A")
self.stream_threshold_status[stream_id] = subflow_entry.get("is_above_threshold", False)
- self.stream_last_active[stream_id] = subflow_entry.get("last_active_time") # 存储原始时间戳
+ self.stream_last_active[stream_id] = subflow_entry.get(
+ "last_changed_state_time"
+ ) # 存储原始时间戳
self.stream_last_interaction[stream_id] = subflow_entry.get(
"last_interaction_time"
) # 存储原始时间戳
diff --git a/requirements.txt b/requirements.txt
index 42e7c319..d75284eb 100644
Binary files a/requirements.txt and b/requirements.txt differ
diff --git a/scripts/run.sh b/scripts/run.sh
index b7ecbc84..9fd3127f 100644
--- a/scripts/run.sh
+++ b/scripts/run.sh
@@ -4,7 +4,7 @@
# 适用于Arch/Ubuntu 24.10/Debian 12/CentOS 9
# 请小心使用任何一键脚本!
-INSTALLER_VERSION="0.0.3-refactor"
+INSTALLER_VERSION="0.0.4-refactor"
LANG=C.UTF-8
# 如无法访问GitHub请修改此处镜像地址
@@ -19,10 +19,10 @@ RESET="\e[0m"
declare -A REQUIRED_PACKAGES=(
["common"]="git sudo python3 curl gnupg"
- ["debian"]="python3-venv python3-pip"
- ["ubuntu"]="python3-venv python3-pip"
- ["centos"]="python3-pip"
- ["arch"]="python-virtualenv python-pip"
+ ["debian"]="python3-venv python3-pip build-essential"
+ ["ubuntu"]="python3-venv python3-pip build-essential"
+ ["centos"]="epel-release python3-pip python3-devel gcc gcc-c++ make"
+ ["arch"]="python-virtualenv python-pip base-devel"
)
# 默认项目目录
diff --git a/src/api/graphql/__init__.py b/src/api/graphql/__init__.py
new file mode 100644
index 00000000..b0efa7f9
--- /dev/null
+++ b/src/api/graphql/__init__.py
@@ -0,0 +1,22 @@
+import strawberry
+
+from fastapi import FastAPI
+from strawberry.fastapi import GraphQLRouter
+
+from src.common.server import global_server
+
+
+@strawberry.type
+class Query:
+ @strawberry.field
+ def hello(self) -> str:
+ return "Hello World"
+
+
+schema = strawberry.Schema(Query)
+
+graphql_app = GraphQLRouter(schema)
+
+fast_api_app: FastAPI = global_server.get_app()
+
+fast_api_app.include_router(graphql_app, prefix="/graphql")
diff --git a/src/api/graphql/schema.py b/src/api/graphql/schema.py
new file mode 100644
index 00000000..2ae28399
--- /dev/null
+++ b/src/api/graphql/schema.py
@@ -0,0 +1 @@
+pass
diff --git a/src/common/logger.py b/src/common/logger.py
index 4347fd97..6ab3505d 100644
--- a/src/common/logger.py
+++ b/src/common/logger.py
@@ -5,7 +5,57 @@ import os
from types import ModuleType
from pathlib import Path
from dotenv import load_dotenv
-# from ..plugins.chat.config import global_config
+
+"""
+日志颜色说明:
+
+1. 主程序(Main)
+浅黄色标题 | 浅黄色消息
+
+2. 海马体(Memory)
+浅黄色标题 | 浅黄色消息
+
+3. PFC(前额叶皮质)
+浅绿色标题 | 浅绿色消息
+
+4. 心情(Mood)
+品红色标题 | 品红色消息
+
+5. 工具使用(Tool)
+品红色标题 | 品红色消息
+
+6. 关系(Relation)
+浅品红色标题 | 浅品红色消息
+
+7. 配置(Config)
+浅青色标题 | 浅青色消息
+
+8. 麦麦大脑袋
+浅绿色标题 | 浅绿色消息
+
+9. 在干嘛
+青色标题 | 青色消息
+
+10. 麦麦组织语言
+浅绿色标题 | 浅绿色消息
+
+11. 见闻(Chat)
+浅蓝色标题 | 绿色消息
+
+12. 表情包(Emoji)
+橙色标题 | 橙色消息 fg #FFD700
+
+13. 子心流
+
+13. 其他模块
+模块名标题 | 对应颜色消息
+
+
+注意:
+1. 级别颜色遵循loguru默认配置
+2. 可通过环境变量修改日志级别
+"""
+
# 加载 .env 文件
env_path = Path(__file__).resolve().parent.parent.parent / ".env"
@@ -88,25 +138,6 @@ MAIN_STYLE_CONFIG = {
},
}
-# 海马体日志样式配置
-MEMORY_STYLE_CONFIG = {
- "advanced": {
- "console_format": (
- "{time:YYYY-MM-DD HH:mm:ss} | "
- "{level: <8} | "
- "海马体 | "
- "{message}"
- ),
- "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 海马体 | {message}",
- },
- "simple": {
- "console_format": (
- "{time:MM-DD HH:mm} | 海马体 | {message}"
- ),
- "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 海马体 | {message}",
- },
-}
-
# pfc配置
PFC_STYLE_CONFIG = {
"advanced": {
@@ -132,13 +163,13 @@ MOOD_STYLE_CONFIG = {
"console_format": (
"{time:YYYY-MM-DD HH:mm:ss} | "
"{level: <8} | "
- "心情 | "
+ "心情 | "
"{message}"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 心情 | {message}",
},
"simple": {
- "console_format": "{time:MM-DD HH:mm} | 心情 | {message}",
+ "console_format": "{time:MM-DD HH:mm} | 心情 | {message} ",
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 心情 | {message}",
},
}
@@ -284,15 +315,13 @@ CHAT_STYLE_CONFIG = {
"console_format": (
"{time:YYYY-MM-DD HH:mm:ss} | "
"{level: <8} | "
- "见闻 | "
+ "见闻 | "
"{message}"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 见闻 | {message}",
},
"simple": {
- "console_format": (
- "{time:MM-DD HH:mm} | 见闻 | {message}"
- ), # noqa: E501
+ "console_format": ("{time:MM-DD HH:mm} | 见闻 | {message}"), # noqa: E501
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 见闻 | {message}",
},
}
@@ -314,6 +343,22 @@ REMOTE_STYLE_CONFIG = {
}
SUB_HEARTFLOW_STYLE_CONFIG = {
+ "advanced": {
+ "console_format": (
+ "{time:YYYY-MM-DD HH:mm:ss} | "
+ "{level: <8} | "
+ "麦麦水群 | "
+ "{message}"
+ ),
+ "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}",
+ },
+ "simple": {
+ "console_format": ("{time:MM-DD HH:mm} | 麦麦水群 | {message}"), # noqa: E501
+ "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群 | {message}",
+ },
+}
+
+SUB_HEARTFLOW_MIND_STYLE_CONFIG = {
"advanced": {
"console_format": (
"{time:YYYY-MM-DD HH:mm:ss} | "
@@ -324,13 +369,27 @@ SUB_HEARTFLOW_STYLE_CONFIG = {
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}",
},
"simple": {
- "console_format": (
- "{time:MM-DD HH:mm} | 麦麦小脑袋 | {message}"
- ), # noqa: E501
+ "console_format": ("{time:MM-DD HH:mm} | 麦麦小脑袋 | {message}"), # noqa: E501
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}",
},
}
+SUBHEARTFLOW_MANAGER_STYLE_CONFIG = {
+ "advanced": {
+ "console_format": (
+ "{time:YYYY-MM-DD HH:mm:ss} | "
+ "{level: <8} | "
+ "麦麦水群[管理] | "
+ "{message}"
+ ),
+ "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群[管理] | {message}",
+ },
+ "simple": {
+ "console_format": ("{time:MM-DD HH:mm} | 麦麦水群[管理] | {message}"), # noqa: E501
+ "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群[管理] | {message}",
+ },
+}
+
BASE_TOOL_STYLE_CONFIG = {
"advanced": {
"console_format": (
@@ -349,6 +408,24 @@ BASE_TOOL_STYLE_CONFIG = {
},
}
+CHAT_STREAM_STYLE_CONFIG = {
+ "advanced": {
+ "console_format": (
+ "{time:YYYY-MM-DD HH:mm:ss} | "
+ "{level: <8} | "
+ "聊天流 | "
+ "{message}"
+ ),
+ "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 聊天流 | {message}",
+ },
+ "simple": {
+ "console_format": (
+ "{time:MM-DD HH:mm} | 聊天流 | {message}"
+ ),
+ "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 聊天流 | {message}",
+ },
+}
+
PERSON_INFO_STYLE_CONFIG = {
"advanced": {
"console_format": (
@@ -385,24 +462,6 @@ BACKGROUND_TASKS_STYLE_CONFIG = {
},
}
-SUBHEARTFLOW_MANAGER_STYLE_CONFIG = {
- "advanced": {
- "console_format": (
- "{time:YYYY-MM-DD HH:mm:ss} | "
- "{level: <8} | "
- "小脑袋管理 | "
- "{message}"
- ),
- "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 小脑袋管理 | {message}",
- },
- "simple": {
- "console_format": (
- "{time:MM-DD HH:mm} | 小脑袋管理 | {message}"
- ), # noqa: E501
- "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 小脑袋管理 | {message}",
- },
-}
-
WILLING_STYLE_CONFIG = {
"advanced": {
"console_format": (
@@ -419,19 +478,36 @@ WILLING_STYLE_CONFIG = {
},
}
+PFC_ACTION_PLANNER_STYLE_CONFIG = {
+ "advanced": {
+ "console_format": (
+ "{time:YYYY-MM-DD HH:mm:ss} | "
+ "{level: <8} | "
+ "PFC私聊规划 | "
+ "{message}"
+ ),
+ "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | PFC私聊规划 | {message}",
+ },
+ "simple": {
+ "console_format": "{time:MM-DD HH:mm} | PFC私聊规划 | {message} ", # noqa: E501
+ "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | PFC私聊规划 | {message}",
+ },
+}
+
+# EMOJI,橙色,全着色
EMOJI_STYLE_CONFIG = {
"advanced": {
"console_format": (
"{time:YYYY-MM-DD HH:mm:ss} | "
"{level: <8} | "
- "表情 | "
+ "表情包 | "
"{message}"
),
- "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": {
- "console_format": "{time:MM-DD HH:mm} | 表情 | {message} ", # noqa: E501
- "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 表情 | {message}",
+ "console_format": "{time:MM-DD HH:mm} | 表情包 | {message} ", # noqa: E501
+ "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 表情包 | {message}",
},
}
@@ -446,11 +522,32 @@ MAI_STATE_CONFIG = {
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦状态 | {message}",
},
"simple": {
- "console_format": "{time:MM-DD HH:mm} | 麦麦状态 | {message} ", # noqa: E501
+ "console_format": "{time:MM-DD HH:mm} | 麦麦状态 | {message} ", # noqa: E501
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦状态 | {message}",
},
}
+
+# 海马体日志样式配置
+MEMORY_STYLE_CONFIG = {
+ "advanced": {
+ "console_format": (
+ "{time:YYYY-MM-DD HH:mm:ss} | "
+ "{level: <8} | "
+ "海马体 | "
+ "{message}"
+ ),
+ "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 海马体 | {message}",
+ },
+ "simple": {
+ "console_format": (
+ "{time:MM-DD HH:mm} | 海马体 | {message}"
+ ),
+ "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 海马体 | {message}",
+ },
+}
+
+
# LPMM配置
LPMM_STYLE_CONFIG = {
"advanced": {
@@ -464,7 +561,7 @@ LPMM_STYLE_CONFIG = {
},
"simple": {
"console_format": (
- "{time:MM-DD HH:mm} | LPMM | {message}"
+ "{time:MM-DD HH:mm} | LPMM | {message}"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | LPMM | {message}",
},
@@ -494,9 +591,31 @@ CONFIRM_STYLE_CONFIG = {
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | EULA与PRIVACY确认 | {message}",
}
+# 天依蓝配置
+TIANYI_STYLE_CONFIG = {
+ "advanced": {
+ "console_format": (
+ "{time:YYYY-MM-DD HH:mm:ss} | "
+ "{level: <8} | "
+ "天依 | "
+ "{message}"
+ ),
+ "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 天依 | {message}",
+ },
+ "simple": {
+ "console_format": (
+ "{time:MM-DD HH:mm} | 天依 | {message}"
+ ),
+ "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 天依 | {message}",
+ },
+}
+
# 根据SIMPLE_OUTPUT选择配置
MAIN_STYLE_CONFIG = MAIN_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else MAIN_STYLE_CONFIG["advanced"]
EMOJI_STYLE_CONFIG = EMOJI_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else EMOJI_STYLE_CONFIG["advanced"]
+PFC_ACTION_PLANNER_STYLE_CONFIG = (
+ PFC_ACTION_PLANNER_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else PFC_ACTION_PLANNER_STYLE_CONFIG["advanced"]
+)
REMOTE_STYLE_CONFIG = REMOTE_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else REMOTE_STYLE_CONFIG["advanced"]
BASE_TOOL_STYLE_CONFIG = BASE_TOOL_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else BASE_TOOL_STYLE_CONFIG["advanced"]
PERSON_INFO_STYLE_CONFIG = PERSON_INFO_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else PERSON_INFO_STYLE_CONFIG["advanced"]
@@ -507,6 +626,7 @@ BACKGROUND_TASKS_STYLE_CONFIG = (
BACKGROUND_TASKS_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else BACKGROUND_TASKS_STYLE_CONFIG["advanced"]
)
MEMORY_STYLE_CONFIG = MEMORY_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else MEMORY_STYLE_CONFIG["advanced"]
+CHAT_STREAM_STYLE_CONFIG = CHAT_STREAM_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else CHAT_STREAM_STYLE_CONFIG["advanced"]
TOPIC_STYLE_CONFIG = TOPIC_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else TOPIC_STYLE_CONFIG["advanced"]
SENDER_STYLE_CONFIG = SENDER_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else SENDER_STYLE_CONFIG["advanced"]
LLM_STYLE_CONFIG = LLM_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else LLM_STYLE_CONFIG["advanced"]
@@ -518,6 +638,9 @@ HEARTFLOW_STYLE_CONFIG = HEARTFLOW_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else
SUB_HEARTFLOW_STYLE_CONFIG = (
SUB_HEARTFLOW_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else SUB_HEARTFLOW_STYLE_CONFIG["advanced"]
) # noqa: E501
+SUB_HEARTFLOW_MIND_STYLE_CONFIG = (
+ SUB_HEARTFLOW_MIND_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else SUB_HEARTFLOW_MIND_STYLE_CONFIG["advanced"]
+)
WILLING_STYLE_CONFIG = WILLING_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else WILLING_STYLE_CONFIG["advanced"]
MAI_STATE_CONFIG = MAI_STATE_CONFIG["simple"] if SIMPLE_OUTPUT else MAI_STATE_CONFIG["advanced"]
CONFIG_STYLE_CONFIG = CONFIG_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else CONFIG_STYLE_CONFIG["advanced"]
@@ -525,6 +648,7 @@ TOOL_USE_STYLE_CONFIG = TOOL_USE_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else TO
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"]
INTEREST_STYLE_CONFIG = INTEREST_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else INTEREST_STYLE_CONFIG["advanced"]
+TIANYI_STYLE_CONFIG = TIANYI_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else TIANYI_STYLE_CONFIG["advanced"]
def is_registered_module(record: dict) -> bool:
diff --git a/src/config/config.py b/src/config/config.py
index ba9416d5..2ade83f1 100644
--- a/src/config/config.py
+++ b/src/config/config.py
@@ -28,7 +28,7 @@ logger = get_module_logger("config", config=config_config)
# 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码
is_test = True
mai_version_main = "0.6.3"
-mai_version_fix = "snapshot-4"
+mai_version_fix = "snapshot-5"
if mai_version_fix:
if is_test:
@@ -192,7 +192,6 @@ class BotConfig:
reply_trigger_threshold: float = 3.0 # 心流聊天触发阈值,越低越容易触发
probability_decay_factor_per_second: float = 0.2 # 概率衰减因子,越大衰减越快
default_decay_rate_per_second: float = 0.98 # 默认衰减率,越大衰减越慢
- initial_duration: int = 60 # 初始持续时间,越大心流聊天持续的时间越长
# sub_heart_flow_update_interval: int = 60 # 子心流更新频率,间隔 单位秒
# sub_heart_flow_freeze_time: int = 120 # 子心流冻结时间,超过这个时间没有回复,子心流会冻结,间隔 单位秒
@@ -221,8 +220,11 @@ class BotConfig:
max_emoji_num: int = 200 # 表情包最大数量
max_reach_deletion: bool = True # 开启则在达到最大数量时删除表情包,关闭则不会继续收集表情包
EMOJI_CHECK_INTERVAL: int = 120 # 表情包检查间隔(分钟)
- EMOJI_REGISTER_INTERVAL: int = 10 # 表情包注册间隔(分钟)
- EMOJI_SAVE: bool = True # 偷表情包
+
+ save_pic: bool = False # 是否保存图片
+ save_emoji: bool = False # 是否保存表情包
+ steal_emoji: bool = True # 是否偷取表情包,让麦麦可以发送她保存的这些表情包
+
EMOJI_CHECK: bool = False # 是否开启过滤
EMOJI_CHECK_PROMPT: str = "符合公序良俗" # 表情包过滤要求
@@ -259,6 +261,7 @@ class BotConfig:
chinese_typo_word_replace_rate = 0.02 # 整词替换概率
# response_splitter
+ enable_kaomoji_protection = False # 是否启用颜文字保护
enable_response_splitter = True # 是否启用回复分割器
response_max_length = 100 # 回复允许的最大长度
response_max_sentence_num = 3 # 回复允许的最大句子数
@@ -282,10 +285,11 @@ class BotConfig:
vlm: Dict[str, str] = field(default_factory=lambda: {})
moderation: Dict[str, str] = field(default_factory=lambda: {})
- # 实验性
llm_observation: Dict[str, str] = field(default_factory=lambda: {})
llm_sub_heartflow: Dict[str, str] = field(default_factory=lambda: {})
llm_heartflow: Dict[str, str] = field(default_factory=lambda: {})
+ llm_tool_use: Dict[str, str] = field(default_factory=lambda: {})
+ llm_plan: Dict[str, str] = field(default_factory=lambda: {})
api_urls: Dict[str, str] = field(default_factory=lambda: {})
@@ -389,13 +393,15 @@ class BotConfig:
def emoji(parent: dict):
emoji_config = parent["emoji"]
config.EMOJI_CHECK_INTERVAL = emoji_config.get("check_interval", config.EMOJI_CHECK_INTERVAL)
- config.EMOJI_REGISTER_INTERVAL = emoji_config.get("register_interval", config.EMOJI_REGISTER_INTERVAL)
config.EMOJI_CHECK_PROMPT = emoji_config.get("check_prompt", config.EMOJI_CHECK_PROMPT)
- config.EMOJI_SAVE = emoji_config.get("auto_save", config.EMOJI_SAVE)
config.EMOJI_CHECK = emoji_config.get("enable_check", config.EMOJI_CHECK)
if config.INNER_VERSION in SpecifierSet(">=1.1.1"):
config.max_emoji_num = emoji_config.get("max_emoji_num", config.max_emoji_num)
config.max_reach_deletion = emoji_config.get("max_reach_deletion", config.max_reach_deletion)
+ if config.INNER_VERSION in SpecifierSet(">=1.4.2"):
+ config.save_pic = emoji_config.get("save_pic", config.save_pic)
+ config.save_emoji = emoji_config.get("save_emoji", config.save_emoji)
+ config.steal_emoji = emoji_config.get("steal_emoji", config.steal_emoji)
def bot(parent: dict):
# 机器人基础配置
@@ -420,21 +426,9 @@ class BotConfig:
def heartflow(parent: dict):
heartflow_config = parent["heartflow"]
- # 加载新增的 heartflowC 参数
-
- # 加载原有的 heartflow 参数
- # config.sub_heart_flow_update_interval = heartflow_config.get(
- # "sub_heart_flow_update_interval", config.sub_heart_flow_update_interval
- # )
- # config.sub_heart_flow_freeze_time = heartflow_config.get(
- # "sub_heart_flow_freeze_time", config.sub_heart_flow_freeze_time
- # )
config.sub_heart_flow_stop_time = heartflow_config.get(
"sub_heart_flow_stop_time", config.sub_heart_flow_stop_time
)
- # config.heart_flow_update_interval = heartflow_config.get(
- # "heart_flow_update_interval", config.heart_flow_update_interval
- # )
if config.INNER_VERSION in SpecifierSet(">=1.3.0"):
config.observation_context_size = heartflow_config.get(
"observation_context_size", config.observation_context_size
@@ -453,7 +447,6 @@ class BotConfig:
config.default_decay_rate_per_second = heartflow_config.get(
"default_decay_rate_per_second", config.default_decay_rate_per_second
)
- config.initial_duration = heartflow_config.get("initial_duration", config.initial_duration)
def willing(parent: dict):
willing_config = parent["willing"]
@@ -494,7 +487,11 @@ class BotConfig:
"llm_tool_use",
"llm_observation",
"llm_sub_heartflow",
+ "llm_plan",
"llm_heartflow",
+ "llm_PFC_action_planner",
+ "llm_PFC_chat",
+ "llm_PFC_reply_checker",
]
for item in config_list:
@@ -643,6 +640,10 @@ class BotConfig:
config.response_max_sentence_num = response_splitter_config.get(
"response_max_sentence_num", config.response_max_sentence_num
)
+ if config.INNER_VERSION in SpecifierSet(">=1.4.2"):
+ config.enable_kaomoji_protection = response_splitter_config.get(
+ "enable_kaomoji_protection", config.enable_kaomoji_protection
+ )
def groups(parent: dict):
groups_config = parent["groups"]
diff --git a/src/do_tool/tool_can_use/base_tool.py b/src/do_tool/tool_can_use/base_tool.py
index 572a037b..30ab4c6c 100644
--- a/src/do_tool/tool_can_use/base_tool.py
+++ b/src/do_tool/tool_can_use/base_tool.py
@@ -42,13 +42,11 @@ class BaseTool(ABC):
"function": {"name": cls.name, "description": cls.description, "parameters": cls.parameters},
}
- @abstractmethod
- async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]:
+ async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]:
"""执行工具函数
Args:
function_args: 工具调用参数
- message_txt: 原始消息文本
Returns:
Dict: 工具执行结果
diff --git a/src/do_tool/tool_can_use/compare_numbers_tool.py b/src/do_tool/tool_can_use/compare_numbers_tool.py
index b2f5dd18..4d030aeb 100644
--- a/src/do_tool/tool_can_use/compare_numbers_tool.py
+++ b/src/do_tool/tool_can_use/compare_numbers_tool.py
@@ -19,7 +19,7 @@ class CompareNumbersTool(BaseTool):
"required": ["num1", "num2"],
}
- async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]:
+ async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]:
"""执行比较两个数的大小
Args:
diff --git a/src/do_tool/tool_can_use/get_knowledge.py b/src/do_tool/tool_can_use/get_knowledge.py
index 0ccac52c..600afd36 100644
--- a/src/do_tool/tool_can_use/get_knowledge.py
+++ b/src/do_tool/tool_can_use/get_knowledge.py
@@ -21,7 +21,7 @@ class SearchKnowledgeTool(BaseTool):
"required": ["query"],
}
- async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]:
+ async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]:
"""执行知识库搜索
Args:
@@ -32,7 +32,7 @@ class SearchKnowledgeTool(BaseTool):
Dict: 工具执行结果
"""
try:
- query = function_args.get("query", message_txt)
+ query = function_args.get("query")
threshold = function_args.get("threshold", 0.4)
# 调用知识库搜索
diff --git a/src/do_tool/tool_can_use/get_memory.py b/src/do_tool/tool_can_use/get_memory.py
index 28346d46..98a4e85e 100644
--- a/src/do_tool/tool_can_use/get_memory.py
+++ b/src/do_tool/tool_can_use/get_memory.py
@@ -20,7 +20,7 @@ class GetMemoryTool(BaseTool):
"required": ["topic"],
}
- async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]:
+ async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]:
"""执行记忆获取
Args:
@@ -31,7 +31,7 @@ class GetMemoryTool(BaseTool):
Dict: 工具执行结果
"""
try:
- topic = function_args.get("topic", message_txt)
+ topic = function_args.get("topic")
max_memory_num = function_args.get("max_memory_num", 2)
# 将主题字符串转换为列表
diff --git a/src/do_tool/tool_can_use/get_time_date.py b/src/do_tool/tool_can_use/get_time_date.py
index b54210fc..ef1f00f0 100644
--- a/src/do_tool/tool_can_use/get_time_date.py
+++ b/src/do_tool/tool_can_use/get_time_date.py
@@ -16,7 +16,7 @@ class GetCurrentDateTimeTool(BaseTool):
"required": [],
}
- async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]:
+ async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]:
"""执行获取当前时间、日期、年份和星期
Args:
diff --git a/src/do_tool/tool_can_use/lpmm_get_knowledge.py b/src/do_tool/tool_can_use/lpmm_get_knowledge.py
index 601d6083..7541d48a 100644
--- a/src/do_tool/tool_can_use/lpmm_get_knowledge.py
+++ b/src/do_tool/tool_can_use/lpmm_get_knowledge.py
@@ -24,7 +24,7 @@ class SearchKnowledgeFromLPMMTool(BaseTool):
"required": ["query"],
}
- async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]:
+ async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]:
"""执行知识库搜索
Args:
@@ -35,7 +35,7 @@ class SearchKnowledgeFromLPMMTool(BaseTool):
Dict: 工具执行结果
"""
try:
- query = function_args.get("query", message_txt)
+ query = function_args.get("query")
# threshold = function_args.get("threshold", 0.4)
# 调用知识库搜索
diff --git a/src/do_tool/tool_use.py b/src/do_tool/tool_use.py
index 019294ec..8087ceda 100644
--- a/src/do_tool/tool_use.py
+++ b/src/do_tool/tool_use.py
@@ -50,8 +50,8 @@ class ToolUser:
prompt += message_txt
# prompt += f"你注意到{sender_name}刚刚说:{message_txt}\n"
prompt += f"注意你就是{bot_name},{bot_name}是你的名字。根据之前的聊天记录补充问题信息,搜索时避开你的名字。\n"
- prompt += "必须调用 'lpmm_get_knowledge' 工具来获取知识。\n"
- prompt += "你现在需要对群里的聊天内容进行回复,现在选择工具来对消息和你的回复进行处理,你是否需要额外的信息,比如回忆或者搜寻已有的知识,改变关系和情感,或者了解你现在正在做什么。"
+ # prompt += "必须调用 'lpmm_get_knowledge' 工具来获取知识。\n"
+ prompt += "你现在需要对群里的聊天内容进行回复,请你思考应该使用什么工具,然后选择工具来对消息和你的回复进行处理,你是否需要额外的信息,比如回忆或者搜寻已有的知识,改变关系和情感,或者了解你现在正在做什么。"
prompt = await relationship_manager.convert_all_person_sign_to_person_name(prompt)
prompt = parse_text_timestamps(prompt, mode="lite")
@@ -68,7 +68,7 @@ class ToolUser:
return get_all_tool_definitions()
@staticmethod
- async def _execute_tool_call(tool_call, message_txt: str):
+ async def _execute_tool_call(tool_call):
"""执行特定的工具调用
Args:
@@ -89,7 +89,7 @@ class ToolUser:
return None
# 执行工具
- result = await tool_instance.execute(function_args, message_txt)
+ result = await tool_instance.execute(function_args)
if result:
# 直接使用 function_name 作为 tool_type
tool_type = function_name
@@ -159,13 +159,15 @@ class ToolUser:
tool_calls_str = ""
for tool_call in tool_calls:
tool_calls_str += f"{tool_call['function']['name']}\n"
- logger.info(f"根据:\n{prompt}\n模型请求调用{len(tool_calls)}个工具: {tool_calls_str}")
+ logger.info(
+ f"根据:\n{prompt}\n\n内容:{content}\n\n模型请求调用{len(tool_calls)}个工具: {tool_calls_str}"
+ )
tool_results = []
structured_info = {} # 动态生成键
# 执行所有工具调用
for tool_call in tool_calls:
- result = await self._execute_tool_call(tool_call, message_txt)
+ result = await self._execute_tool_call(tool_call)
if result:
tool_results.append(result)
# 使用工具名称作为键
diff --git a/src/heart_flow/README.md b/src/heart_flow/README.md
index dc00a9ff..24d094cc 100644
--- a/src/heart_flow/README.md
+++ b/src/heart_flow/README.md
@@ -1,157 +1,223 @@
# 心流系统 (Heart Flow System)
-## 系统架构
+## 一条消息是怎么到最终回复的?简明易懂的介绍
-### 1. 主心流 (Heartflow)
-- 位于 `heartflow.py`
-- 作为整个系统的主控制器
-- 负责管理和协调多个子心流
-- 维护AI的整体思维状态
-- 定期进行全局思考更新
+1 接受消息,由HeartHC_processor处理消息,存储消息
-### 2. 子心流 (SubHeartflow)
-- 位于 `sub_heartflow.py`
-- 处理具体的对话场景(如群聊)
-- 维护特定场景下的思维状态
-- 通过观察者模式接收和处理信息
-- 能够进行独立的思考和回复判断
+ 1.1 process_message()函数,接受消息
-### 3. 观察系统 (Observation)
-- 位于 `observation.py`
-- 负责收集和处理外部信息
-- 支持多种观察类型(如聊天观察)
-- 对信息进行实时总结和更新
+ 1.2 创建消息对应的聊天流(chat_stream)和子心流(sub_heartflow)
+ 1.3 进行常规消息处理
-## 工作流程
+ 1.4 存储消息 store_message()
-1. 主心流启动并创建必要的子心流
-2. 子心流通过观察者接收外部信息
-3. 系统进行信息处理和思维更新
-4. 根据情感状态和思维结果决定是否回复
-5. 生成合适的回复并更新思维状态
+ 1.5 计算兴趣度Interest
-## 使用说明
+ 1.6 将消息连同兴趣度,存储到内存中的interest_dict(SubHeartflow的属性)
-### 创建新的子心流
-```python
-heartflow = Heartflow()
-subheartflow = heartflow.create_subheartflow(chat_id)
-```
+2 根据 sub_heartflow 的聊天状态,决定后续处理流程
-### 添加观察者
-```python
-observation = ChattingObservation(chat_id)
-subheartflow.add_observation(observation)
-```
+ 2a ABSENT状态:不做任何处理
-## 配置说明
+ 2b CHAT状态:送入NormalChat 实例
-系统的主要配置参数:
-- `sub_heart_flow_stop_time`: 子心流停止时间
-- `sub_heart_flow_freeze_time`: 子心流冻结时间
-- `heart_flow_update_interval`: 心流更新间隔
+ 2c FOCUS状态:送入HeartFChatting 实例
-## 注意事项
+b NormalChat工作方式
-1. 子心流会在长时间不活跃后自动清理
-2. 需要合理配置更新间隔以平衡性能和响应速度
-3. 观察系统会限制消息处理数量以避免过载
+ b.1 启动后台任务 _reply_interested_message,持续运行。
+ b.2 该任务轮询 InterestChatting 提供的 interest_dict
+ b.3 对每条消息,结合兴趣度、是否被提及(@)、意愿管理器(WillingManager)计算回复概率。(这部分要改,目前还是用willing计算的,之后要和Interest合并)
+ b.4 若概率通过:
+ b.4.1 创建"思考中"消息 (MessageThinking)。
+ b.4.2 调用 NormalChatGenerator 生成文本回复。
+ b.4.3 通过 message_manager 发送回复 (MessageSending)。
+ b.4.4 可能根据配置和文本内容,额外发送一个匹配的表情包。
+ b.4.5 更新关系值和全局情绪。
+ b.5 处理完成后,从 interest_dict 中移除该消息。
-# HeartFChatting 与主动回复流程说明 (V2)
+c HeartFChatting工作方式
-本文档描述了 `HeartFChatting` 类及其在 `heartFC_controler` 模块中实现的主动、基于兴趣的回复流程。
-
-## 1. `HeartFChatting` 类概述
-
-* **目标**: 管理特定聊天流 (`stream_id`) 的主动回复逻辑,使其行为更像人类的自然交流。
-* **创建时机**: 当 `HeartFC_Chat` 的兴趣监控任务 (`_interest_monitor_loop`) 检测到某个聊天流的兴趣度 (`InterestChatting`) 达到了触发回复评估的条件 (`should_evaluate_reply`) 时,会为该 `stream_id` 获取或创建唯一的 `HeartFChatting` 实例 (`_get_or_create_heartFC_chat`)。
-* **持有**:
- * 对应的 `sub_heartflow` 实例引用 (通过 `heartflow.get_subheartflow(stream_id)`)。
- * 对应的 `chat_stream` 实例引用。
- * 对 `HeartFC_Chat` 单例的引用 (用于调用发送消息、处理表情等辅助方法)。
-* **初始化**: `HeartFChatting` 实例在创建后会执行异步初始化 (`_initialize`),这可能包括加载必要的上下文或历史信息(*待确认是否实现了读取历史消息*)。
-
-## 2. 核心回复流程 (由 `HeartFC_Chat` 触发)
-
-当 `HeartFC_Chat` 调用 `HeartFChatting` 实例的方法 (例如 `add_time`) 时,会启动内部的回复决策与执行流程:
-
-1. **规划 (Planner):**
- * **输入**: 从关联的 `sub_heartflow` 获取观察结果、思考链、记忆片段等上下文信息。
- * **决策**:
- * 判断当前是否适合进行回复。
- * 决定回复的形式(纯文本、带表情包等)。
- * 选择合适的回复时机和策略。
- * **实现**: *此部分逻辑待详细实现,可能利用 LLM 的工具调用能力来增强决策的灵活性和智能性。需要考虑机器人的个性化设定。*
-
-2. **回复生成 (Replier):**
- * **输入**: Planner 的决策结果和必要的上下文。
- * **执行**:
- * 调用 `ResponseGenerator` (`self.gpt`) 或类似组件生成具体的回复文本内容。
- * 可能根据 Planner 的策略生成多个候选回复。
- * **并发**: 系统支持同时存在多个思考/生成任务(上限由 `global_config.max_concurrent_thinking_messages` 控制)。
-
-3. **检查 (Checker):**
- * **时机**: 在回复生成过程中或生成后、发送前执行。
- * **目的**:
- * 检查自开始生成回复以来,聊天流中是否出现了新的消息。
- * 评估已生成的候选回复在新的上下文下是否仍然合适、相关。
- * *需要实现相似度比较逻辑,防止发送与近期消息内容相近或重复的回复。*
- * **处理**: 如果检查结果认为回复不合适,则该回复将被**抛弃**。
-
-4. **发送协调:**
- * **执行**: 如果 Checker 通过,`HeartFChatting` 会调用 `HeartFC_Chat` 实例提供的发送接口:
- * `_create_thinking_message`: 通知 `MessageManager` 显示"正在思考"状态。
- * `_send_response_messages`: 将最终的回复文本交给 `MessageManager` 进行排队和发送。
- * `_handle_emoji`: 如果需要发送表情包,调用此方法处理表情包的获取和发送。
- * **细节**: 实际的消息发送、排队、间隔控制由 `MessageManager` 和 `MessageSender` 负责。
-
-## 3. 与其他模块的交互
-
-* **`HeartFC_Chat`**:
- * 创建、管理和触发 `HeartFChatting` 实例。
- * 提供发送消息 (`_send_response_messages`)、处理表情 (`_handle_emoji`)、创建思考消息 (`_create_thinking_message`) 的接口给 `HeartFChatting` 调用。
- * 运行兴趣监控循环 (`_interest_monitor_loop`)。
-* **`InterestManager` / `InterestChatting`**:
- * `InterestManager` 存储每个 `stream_id` 的 `InterestChatting` 实例。
- * `InterestChatting` 负责计算兴趣衰减和回复概率。
- * `HeartFC_Chat` 查询 `InterestChatting.should_evaluate_reply()` 来决定是否触发 `HeartFChatting`。
-* **`heartflow` / `sub_heartflow`**:
- * `HeartFChatting` 从对应的 `sub_heartflow` 获取进行规划所需的核心上下文信息 (观察、思考链等)。
-* **`MessageManager` / `MessageSender`**:
- * 接收来自 `HeartFC_Chat` 的发送请求 (思考消息、文本消息、表情包消息)。
- * 管理消息队列 (`MessageContainer`),处理消息发送间隔和实际发送 (`MessageSender`)。
-* **`ResponseGenerator` (`gpt`)**:
- * 被 `HeartFChatting` 的 Replier 部分调用,用于生成回复文本。
-* **`MessageStorage`**:
- * 存储所有接收和发送的消息。
-* **`HippocampusManager`**:
- * `HeartFC_Processor` 使用它计算传入消息的记忆激活率,作为兴趣度计算的输入之一。
-
-## 4. 原有问题与状态更新
-
-1. **每个 `pfchating` 是否对应一个 `chat_stream`,是否是唯一的?**
- * **是**。`HeartFC_Chat._get_or_create_heartFC_chat` 确保了每个 `stream_id` 只有一个 `HeartFChatting` 实例。 (已确认)
-2. **`observe_text` 传入进来是纯 str,是不是应该传进来 message 构成的 list?**
- * **机制已改变**。当前的触发机制是基于 `InterestManager` 的概率判断。`HeartFChatting` 启动后,应从其关联的 `sub_heartflow` 获取更丰富的上下文信息,而非简单的 `observe_text`。
-3. **检查失败的回复应该怎么处理?**
- * **暂定:抛弃**。这是当前 Checker 逻辑的基础设定。
-4. **如何比较相似度?**
- * **待实现**。Checker 需要具体的算法来比较候选回复与新消息的相似度。
-5. **Planner 怎么写?**
- * **待实现**。这是 `HeartFChatting` 的核心决策逻辑,需要结合 `sub_heartflow` 的输出、LLM 工具调用和个性化配置来设计。
-
-
-## 6. 未来优化点
-
-* 实现 Checker 中的相似度比较算法。
-* 详细设计并实现 Planner 的决策逻辑,包括 LLM 工具调用和个性化。
-* 确认并完善 `HeartFChatting._initialize()` 中的历史消息加载逻辑。
-* 探索更优的检查失败回复处理策略(例如:重新规划、修改回复等)。
-* 优化 `HeartFChatting` 与 `sub_heartflow` 的信息交互。
+ c.1 启动主循环 _hfc_loop
+ c.2 每个循环称为一个周期 (Cycle),执行 think_plan_execute 流程。
+ c.3 Think (思考) 阶段:
+ c.3.1 观察 (Observe): 通过 ChattingObservation,使用 observe() 获取最新的聊天消息。
+ c.3.2 思考 (Think): 调用 SubMind 的 do_thinking_before_reply 方法。
+ c.3.2.1 SubMind 结合观察到的内容、个性、情绪、上周期动作等信息,生成当前的内心想法 (current_mind)。
+ c.3.2.2 在此过程中 SubMind 的LLM可能请求调用工具 (ToolUser) 来获取额外信息或执行操作,结果存储在 structured_info 中。
+ c.4 Plan (规划/决策) 阶段:
+ c.4.1 结合观察到的消息文本、`SubMind` 生成的 `current_mind` 和 `structured_info`、以及 `ActionManager` 提供的可用动作,决定本次周期的行动 (`text_reply`/`emoji_reply`/`no_reply`) 和理由。
+ c.4.2 重新规划检查 (Re-plan Check): 如果在 c.3.1 到 c.4.1 期间检测到新消息,可能(有概率)触发重新执行 c.4.1 决策步骤。
+ c.5 Execute (执行/回复) 阶段:
+ c.5.1 如果决策是 text_reply:
+ c.5.1.1 获取锚点消息。
+ c.5.1.2 通过 HeartFCSender 注册"思考中"状态。
+ c.5.1.3 调用 HeartFCGenerator (gpt_instance) 生成回复文本。
+ c.5.1.4 通过 HeartFCSender 发送回复
+ c.5.1.5 如果规划时指定了表情查询 (emoji_query),随后发送表情。
+ c.5.2 如果决策是 emoji_reply:
+ c.5.2.1 获取锚点消息。
+ c.5.2.2 通过 HeartFCSender 直接发送匹配查询 (emoji_query) 的表情。
+ c.5.3 如果决策是 no_reply:
+ c.5.3.1 进入等待状态,直到检测到新消息或超时。
+ c.6 循环结束后,记录周期信息 (CycleInfo),并根据情况进行短暂休眠,防止CPU空转。
-BUG:
-2.复读,可能是planner还未校准好
-3.planner还未个性化,需要加入bot个性信息,且获取的聊天内容有问题
\ No newline at end of file
+## 1. 一条消息是怎么到最终回复的?复杂细致的介绍
+
+### 1.1. 主心流 (Heartflow)
+- **文件**: `heartflow.py`
+- **职责**:
+ - 作为整个系统的主控制器。
+ - 持有并管理 `SubHeartflowManager`,用于管理所有子心流。
+ - 持有并管理自身状态 `self.current_state: MaiStateInfo`,该状态控制系统的整体行为模式。
+ - 统筹管理系统后台任务(如消息存储、资源分配等)。
+ - **注意**: 主心流自身不进行周期性的全局思考更新。
+
+### 1.2. 子心流 (SubHeartflow)
+- **文件**: `sub_heartflow.py`
+- **职责**:
+ - 处理具体的交互场景,例如:群聊、私聊、与虚拟主播(vtb)互动、桌面宠物交互等。
+ - 维护特定场景下的思维状态和聊天流状态 (`ChatState`)。
+ - 通过关联的 `Observation` 实例接收和处理信息。
+ - 拥有独立的思考 (`SubMind`) 和回复判断能力。
+- **观察者**: 每个子心流可以拥有一个或多个 `Observation` 实例(目前每个子心流仅使用一个 `ChattingObservation`)。
+- **内部结构**:
+ - **聊天流状态 (`ChatState`)**: 标记当前子心流的参与模式 (`ABSENT`, `CHAT`, `FOCUSED`),决定是否观察、回复以及使用何种回复模式。
+ - **聊天实例 (`NormalChatInstance` / `HeartFlowChatInstance`)**: 根据 `ChatState` 激活对应的实例来处理聊天逻辑。同一时间只有一个实例处于活动状态。
+
+### 1.3. 观察系统 (Observation)
+- **文件**: `observation.py`
+- **职责**:
+ - 定义信息输入的来源和格式。
+ - 为子心流提供其所处环境的信息。
+- **当前实现**:
+ - 目前仅有 `ChattingObservation` 一种观察类型。
+ - `ChattingObservation` 负责从数据库拉取指定聊天的最新消息,并将其格式化为可读内容,供 `SubHeartflow` 使用。
+
+### 1.4. 子心流管理器 (SubHeartflowManager)
+- **文件**: `subheartflow_manager.py`
+- **职责**:
+ - 作为 `Heartflow` 的成员变量存在。
+ - **在初始化时接收并持有 `Heartflow` 的 `MaiStateInfo` 实例。**
+ - 负责所有 `SubHeartflow` 实例的生命周期管理,包括:
+ - 创建和获取 (`get_or_create_subheartflow`)。
+ - 停止和清理 (`sleep_subheartflow`, `cleanup_inactive_subheartflows`)。
+ - 根据 `Heartflow` 的状态 (`self.mai_state_info`) 和限制条件,激活、停用或调整子心流的状态(例如 `enforce_subheartflow_limits`, `activate_random_subflows_to_chat`, `evaluate_interest_and_promote`)。
+ - **清理机制**: 通过后台任务 (`BackgroundTaskManager`) 定期调用 `cleanup_inactive_subheartflows` 方法,此方法会识别并**删除**那些处于 `ABSENT` 状态超过一小时 (`INACTIVE_THRESHOLD_SECONDS`) 的子心流实例。
+
+### 1.5. 消息处理与回复流程 (Message Processing vs. Replying Flow)
+- **关注点分离**: 系统严格区分了接收和处理传入消息的流程与决定和生成回复的流程。
+ - **消息处理 (Processing)**:
+ - 由一个独立的处理器(例如 `HeartFCProcessor`)负责接收原始消息数据。
+ - 职责包括:消息解析 (`MessageRecv`)、过滤(屏蔽词、正则表达式)、基于记忆系统的初步兴趣计算 (`HippocampusManager`)、消息存储 (`MessageStorage`) 以及用户关系更新 (`RelationshipManager`)。
+ - 处理后的消息信息(如计算出的兴趣度)会传递给对应的 `SubHeartflow`。
+ - **回复决策与生成 (Replying)**:
+ - 由 `SubHeartflow` 及其当前激活的聊天实例 (`NormalChatInstance` 或 `HeartFlowChatInstance`) 负责。
+ - 基于其内部状态 (`ChatState`、`SubMind` 的思考结果)、观察到的信息 (`Observation` 提供的内容) 以及 `InterestChatting` 的状态来决定是否回复、何时回复以及如何回复。
+- **消息缓冲 (Message Caching)**:
+ - `message_buffer` 模块会对某些传入消息进行临时缓存,尤其是在处理连续的多部分消息(如多张图片)时。
+ - 这个缓冲机制发生在 `HeartFCProcessor` 处理流程中,确保消息的完整性,然后才进行后续的存储和兴趣计算。
+ - 缓存的消息最终仍会流向对应的 `ChatStream`(与 `SubHeartflow` 关联),但核心的消息处理与回复决策仍然是分离的步骤。
+
+## 2. 核心控制与状态管理 (Core Control and State Management)
+
+### 2.1. Heart Flow 整体控制
+- **控制者**: 主心流 (`Heartflow`)
+- **核心职责**:
+ - 通过其成员 `SubHeartflowManager` 创建和管理子心流(**在创建 `SubHeartflowManager` 时会传入自身的 `MaiStateInfo`**)。
+ - 通过其成员 `self.current_state: MaiStateInfo` 控制整体行为模式。
+ - 管理系统级后台任务。
+ - **注意**: 不再提供直接获取所有子心流 ID (`get_all_subheartflows_streams_ids`) 的公共方法。
+
+### 2.2. Heart Flow 状态 (`MaiStateInfo`)
+- **定义与管理**: `Heartflow` 持有 `MaiStateInfo` 的实例 (`self.current_state`) 来管理其状态。状态的枚举定义在 `my_state_manager.py` 中的 `MaiState`。
+- **状态及含义**:
+ - `MaiState.OFFLINE` (不在线): 不观察任何群消息,不进行主动交互,仅存储消息。当主状态变为 `OFFLINE` 时,`SubHeartflowManager` 会将所有子心流的状态设置为 `ChatState.ABSENT`。
+ - `MaiState.PEEKING` (看一眼手机): 有限度地参与聊天(由 `MaiStateInfo` 定义具体的普通/专注群数量限制)。
+ - `MaiState.NORMAL_CHAT` (正常看手机): 正常参与聊天,允许 `SubHeartflow` 进入 `CHAT` 或 `FOCUSED` 状态(数量受限)。
+ * `MaiState.FOCUSED_CHAT` (专心看手机): 更积极地参与聊天,通常允许更多或更高优先级的 `FOCUSED` 状态子心流。
+- **作用**: `Heartflow` 的状态直接影响 `SubHeartflowManager` 如何管理子心流(如激活数量、允许的状态等)。
+
+### 2.3. 聊天流状态 (`ChatState`) 与转换
+- **管理对象**: 每个 `SubHeartflow` 实例内部维护其 `ChatStateInfo`,包含当前的 `ChatState`。
+- **状态及含义**:
+ - `ChatState.ABSENT` (不参与/没在看): 初始或停用状态。子心流不观察新信息,不进行思考,也不回复。
+ - `ChatState.CHAT` (随便看看/水群): 普通聊天模式。激活 `NormalChatInstance`。
+ * `ChatState.FOCUSED` (专注/认真水群): 专注聊天模式。激活 `HeartFlowChatInstance`。
+- **选择**: 子心流可以根据外部指令(来自 `SubHeartflowManager`)或内部逻辑(未来的扩展)选择进入 `ABSENT` 状态(不回复不观察),或进入 `CHAT` / `FOCUSED` 中的一种回复模式。
+- **状态转换机制** (由 `SubHeartflowManager` 驱动):
+ - **激活 `CHAT`**: 当 `Heartflow` 状态从 `OFFLINE` 变为允许聊天的状态时,`SubHeartflowManager` 会根据限制(通过 `self.mai_state_info` 获取),选择部分 `ABSENT` 状态的子心流,**检查当前 CHAT 状态数量是否达到上限**,如果未达上限,则调用其 `change_chat_state` 方法将其转换为 `CHAT`。
+ - **激活 `FOCUSED`**: `SubHeartflowManager` 会定期评估处于 `CHAT` 状态的子心流的兴趣度 (`InterestChatting.start_hfc_probability`),若满足条件且**检查当前 FOCUSED 状态数量未达上限**(通过 `self.mai_state_info` 获取限制),则调用 `change_chat_state` 将其提升为 `FOCUSED`。
+ - **停用/回退**: `SubHeartflowManager` 可能因 `Heartflow` 状态变化、达到数量限制、长时间不活跃或随机概率等原因,调用 `change_chat_state` 将子心流状态设置为 `ABSENT` 或从 `FOCUSED` 回退到 `CHAT`。当子心流进入 `ABSENT` 状态后,如果持续一小时不活跃,才会被后台清理任务删除。
+ - **注意**: `change_chat_state` 方法本身只负责执行状态转换和管理内部聊天实例(`NormalChatInstance`/`HeartFlowChatInstance`),不再进行限额检查。限额检查的责任完全由调用方(即 `SubHeartflowManager` 中的相关方法,这些方法会使用内部存储的 `mai_state_info` 来获取限制)承担。
+
+## 3. 聊天实例详解 (Chat Instances Explained)
+
+### 3.1. NormalChatInstance
+- **激活条件**: 对应 `SubHeartflow` 的 `ChatState` 为 `CHAT`。
+- **工作流程**:
+ - 当 `SubHeartflow` 进入 `CHAT` 状态时,`NormalChatInstance` 会被激活。
+ - 实例启动后,会创建一个后台任务 (`_reply_interested_message`)。
+ - 该任务持续监控由 `InterestChatting` 传入的、具有一定兴趣度的消息列表 (`interest_dict`)。
+ - 对列表中的每条消息,结合是否被提及 (`@`)、消息本身的兴趣度以及当前的回复意愿 (`WillingManager`),计算出一个回复概率。
+ - 根据计算出的概率随机决定是否对该消息进行回复。
+ - 如果决定回复,则调用 `NormalChatGenerator` 生成回复内容,并可能附带表情包。
+- **行为特点**:
+ - 回复相对常规、简单。
+ - 不投入过多计算资源。
+ - 侧重于维持基本的交流氛围。
+ - 示例:对问候语、日常分享等进行简单回应。
+
+### 3.2. HeartFlowChatInstance (继承自原 PFC 逻辑)
+- **激活条件**: 对应 `SubHeartflow` 的 `ChatState` 为 `FOCUSED`。
+- **工作流程**:
+ - 基于更复杂的规则(原 PFC 模式)进行深度处理。
+ - 对群内话题进行深入分析。
+ - 可能主动发起相关话题或引导交流。
+- **行为特点**:
+ - 回复更积极、深入。
+ - 投入更多资源参与聊天。
+ - 回复内容可能更详细、有针对性。
+ - 对话题参与度高,能带动交流。
+ - 示例:对复杂或有争议话题阐述观点,并与人互动。
+
+## 4. 工作流程示例 (Example Workflow)
+
+1. **启动**: `Heartflow` 启动,初始化 `MaiStateInfo` (例如 `OFFLINE`) 和 `SubHeartflowManager`。
+2. **状态变化**: 用户操作或内部逻辑使 `Heartflow` 的 `current_state` 变为 `NORMAL_CHAT`。
+3. **管理器响应**: `SubHeartflowManager` 检测到状态变化,根据 `NORMAL_CHAT` 的限制,调用 `get_or_create_subheartflow` 获取或创建子心流,并通过 `change_chat_state` 将部分子心流状态从 `ABSENT` 激活为 `CHAT`。
+4. **子心流激活**: 被激活的 `SubHeartflow` 启动其 `NormalChatInstance`。
+5. **信息接收**: 该 `SubHeartflow` 的 `ChattingObservation` 开始从数据库拉取新消息。
+6. **普通回复**: `NormalChatInstance` 处理观察到的信息,执行普通回复逻辑。
+7. **兴趣评估**: `SubHeartflowManager` 定期评估该子心流的 `InterestChatting` 状态。
+8. **提升状态**: 若兴趣度达标且 `Heartflow` 状态允许,`SubHeartflowManager` 调用该子心流的 `change_chat_state` 将其状态提升为 `FOCUSED`。
+9. **子心流切换**: `SubHeartflow` 内部停止 `NormalChatInstance`,启动 `HeartFlowChatInstance`。
+10. **专注回复**: `HeartFlowChatInstance` 开始根据其逻辑进行更深入的交互。
+11. **状态回落/停用**: 若 `Heartflow` 状态变为 `OFFLINE`,`SubHeartflowManager` 会调用所有活跃子心流的 `change_chat_state(ChatState.ABSENT)`,使其进入 `ABSENT` 状态(它们不会立即被删除,只有在 `ABSENT` 状态持续1小时后才会被清理)。
+
+## 5. 使用与配置 (Usage and Configuration)
+
+### 5.1. 使用说明 (Code Examples)
+- **(内部)创建/获取子心流** (由 `SubHeartflowManager` 调用, 示例):
+ ```python
+ # subheartflow_manager.py (get_or_create_subheartflow 内部)
+ # 注意:mai_states 现在是 self.mai_state_info
+ new_subflow = SubHeartflow(subheartflow_id, self.mai_state_info)
+ await new_subflow.initialize()
+ observation = ChattingObservation(chat_id=subheartflow_id)
+ new_subflow.add_observation(observation)
+ ```
+- **(内部)添加观察者** (由 `SubHeartflowManager` 或 `SubHeartflow` 内部调用):
+ ```python
+ # sub_heartflow.py
+ self.observations.append(observation)
+ ```
+
+### 5.2. 配置参数 (Key Parameters)
+- `sub_heart_flow_stop_time`: (已废弃,现在由 `INACTIVE_THRESHOLD_SECONDS` in `subheartflow_manager.py` 控制) 子心流在 `ABSENT` 状态持续多久后被后台任务清理,默认为 3600 秒 (1 小时)。
+- `sub_heart_flow_freeze_time`: 子心流冻结时间 (当前文档未明确体现,可能需要审阅代码确认)。
+- `heart_flow_update_interval`: 主心流更新其状态或执行管理操作的频率 (需要审阅 `Heartflow` 代码确认)。
+- `
\ No newline at end of file
diff --git a/src/heart_flow/background_tasks.py b/src/heart_flow/background_tasks.py
index 85fb6c50..85b77579 100644
--- a/src/heart_flow/background_tasks.py
+++ b/src/heart_flow/background_tasks.py
@@ -49,7 +49,6 @@ class BackgroundTaskManager:
self.update_interval = update_interval
self.cleanup_interval = cleanup_interval
self.log_interval = log_interval
- self.inactive_threshold = inactive_threshold # For cleanup task
self.interest_eval_interval = interest_eval_interval # 存储兴趣评估间隔
self.random_deactivation_interval = random_deactivation_interval # 存储随机停用间隔
@@ -217,21 +216,33 @@ class BackgroundTaskManager:
current_state == self.mai_state_info.mai_status.OFFLINE
and previous_status != self.mai_state_info.mai_status.OFFLINE
):
- logger.info("[后台任务] 主状态离线,触发子流停用")
+ logger.info("检测到离线,停用所有子心流")
await self.subheartflow_manager.deactivate_all_subflows()
async def _perform_cleanup_work(self):
- """执行一轮子心流清理操作。"""
- flows_to_stop = self.subheartflow_manager.cleanup_inactive_subheartflows(self.inactive_threshold)
- if flows_to_stop:
- logger.info(f"[Background Task Cleanup] Attempting to stop {len(flows_to_stop)} inactive flows...")
- stopped_count = 0
- for flow_id, reason in flows_to_stop:
- if await self.subheartflow_manager.stop_subheartflow(flow_id, f"定期清理: {reason}"):
- stopped_count += 1
- logger.info(f"[Background Task Cleanup] Cleanup cycle finished. Stopped {stopped_count} inactive flows.")
- else:
- logger.debug("[Background Task Cleanup] Cleanup cycle finished. No inactive flows found.")
+ """执行子心流清理任务
+ 1. 获取需要清理的不活跃子心流列表
+ 2. 逐个停止这些子心流
+ 3. 记录清理结果
+ """
+ # 获取需要清理的子心流列表(包含ID和原因)
+ flows_to_stop = self.subheartflow_manager.get_inactive_subheartflows()
+
+ if not flows_to_stop:
+ return # 没有需要清理的子心流直接返回
+
+ logger.info(f"准备删除 {len(flows_to_stop)} 个不活跃(1h)子心流")
+ stopped_count = 0
+
+ # 逐个停止子心流
+ for flow_id in flows_to_stop:
+ success = await self.subheartflow_manager.delete_subflow(flow_id)
+ if success:
+ stopped_count += 1
+ logger.debug(f"[清理任务] 已停止子心流 {flow_id}")
+
+ # 记录最终清理结果
+ logger.info(f"[清理任务] 清理完成, 共停止 {stopped_count}/{len(flows_to_stop)} 个子心流")
async def _perform_logging_work(self):
"""执行一轮状态日志记录。"""
diff --git a/src/heart_flow/chat_state_info.py b/src/heart_flow/chat_state_info.py
new file mode 100644
index 00000000..619f372f
--- /dev/null
+++ b/src/heart_flow/chat_state_info.py
@@ -0,0 +1,17 @@
+from src.plugins.moods.moods import MoodManager
+import enum
+
+
+class ChatState(enum.Enum):
+ ABSENT = "没在看群"
+ CHAT = "随便水群"
+ FOCUSED = "认真水群"
+
+
+class ChatStateInfo:
+ def __init__(self):
+ self.chat_status: ChatState = ChatState.ABSENT
+ self.current_state_time = 120
+
+ self.mood_manager = MoodManager()
+ self.mood = self.mood_manager.get_prompt()
diff --git a/src/heart_flow/heartFC_chatting_logic.md b/src/heart_flow/heartFC_chatting_logic.md
new file mode 100644
index 00000000..1e178a6f
--- /dev/null
+++ b/src/heart_flow/heartFC_chatting_logic.md
@@ -0,0 +1,120 @@
+# HeartFChatting 逻辑详解
+
+`HeartFChatting` 类是心流系统(Heart Flow System)中负责**专注聊天**(`ChatState.FOCUSED`)的核心组件。它的主要职责是在特定的聊天流 (`stream_id`) 中,通过一个持续的 **思考(Think)-规划(Plan)-执行(Execute)** 循环来模拟更自然、更深入的对话交互。当关联的 `SubHeartflow` 状态切换为 `FOCUSED` 时,`HeartFChatting` 实例会被创建并启动;当状态切换为其他(如 `CHAT` 或 `ABSENT`)时,它会被关闭。
+
+## 1. 初始化 (`__init__`, `_initialize`)
+
+- **依赖注入**: 在创建时,`HeartFChatting` 接收 `chat_id`(即 `stream_id`)、关联的 `SubMind` 实例以及 `Observation` 实例列表作为参数。
+- **核心组件**: 内部初始化了几个关键组件:
+ - `ActionManager`: 管理当前循环可用的动作(如回复文本、回复表情、不回复)。
+ - `HeartFCGenerator`: (`self.gpt_instance`) 用于生成回复文本。
+ - `ToolUser`: (`self.tool_user`) 用于执行 `SubMind` 可能请求的工具调用(虽然在此类中主要用于获取工具定义,实际执行由 `SubMind` 完成)。
+ - `HeartFCSender`: (`self.heart_fc_sender`) 专门负责处理消息发送逻辑,包括管理"正在思考"状态。
+ - `LLMRequest`: (`self.planner_llm`) 配置用于执行规划任务的大语言模型请求。
+- **状态变量**:
+ - `_initialized`: 标记是否完成懒初始化。
+ - `_processing_lock`: 异步锁,确保同一时间只有一个完整的"思考-规划-执行"周期在运行。
+ - `_loop_active`: 标记主循环是否正在运行。
+ - `_loop_task`: 指向主循环的 `asyncio.Task` 对象。
+ - `_cycle_history`: 一个双端队列 (`deque`),用于存储最近若干次循环的信息 (`CycleInfo`)。
+ - `_current_cycle`: 当前正在执行的循环信息 (`CycleInfo`)。
+- **懒初始化 (`_initialize`)**:
+ - 在首次需要访问 `ChatStream` 前调用(通常在 `start` 方法中)。
+ - 根据 `stream_id` 从 `chat_manager` 获取对应的 `ChatStream` 实例。
+ - 更新日志前缀,使用聊天流的名称以提高可读性。
+
+## 2. 生命周期管理 (`start`, `shutdown`)
+
+- **启动 (`start`)**:
+ - 外部调用此方法来启动 `HeartFChatting` 的工作流程。
+ - 内部调用 `_start_loop_if_needed` 来安全地启动主循环任务 (`_hfc_loop`)。
+- **关闭 (`shutdown`)**:
+ - 外部调用此方法来优雅地停止 `HeartFChatting`。
+ - 取消正在运行的主循环任务 (`_loop_task`)。
+ - 清理内部状态(如 `_loop_active`, `_loop_task`)。
+ - 释放可能被持有的处理锁 (`_processing_lock`)。
+
+## 3. 核心循环 (`_hfc_loop`)
+
+`_hfc_loop` 是 `HeartFChatting` 的心脏,它以异步方式无限期运行(直到被 `shutdown` 取消),不断执行以下步骤:
+
+1. **创建循环记录**: 初始化一个新的 `CycleInfo` 对象来记录本次循环的详细信息(ID、开始时间、计时器、动作、思考内容等)。
+2. **获取处理锁**: 使用 `_processing_lock` 确保并发安全。
+3. **执行思考-规划-执行**: 调用 `_think_plan_execute_loop` 方法。
+4. **处理循环延迟**: 根据本次循环是否执行了实际动作以及循环耗时,智能地引入短暂的 `asyncio.sleep`,防止 CPU 空转或过于频繁的循环。
+5. **记录循环信息**: 将完成的 `CycleInfo` 存入 `_cycle_history`,并记录详细的日志,包括循环耗时和各阶段计时。
+
+## 4. 思考-规划-执行周期 (`_think_plan_execute_loop`)
+
+这是每个循环内部的核心逻辑,按顺序执行:
+
+### 4.1. 思考阶段 (`_get_submind_thinking`)
+
+1. **触发观察**: 调用关联的 `Observation` 实例的 `observe()` 方法,使其更新对环境(如聊天室新消息)的观察。
+2. **触发子思维**: 调用关联的 `SubMind` 实例的 `do_thinking_before_reply()` 方法。**关键**: 会将上一个循环的 `CycleInfo` 传递给 `SubMind`,使其了解上一次行动的决策、理由以及是否发生了重新规划,从而实现更连贯的思考。
+3. **获取思考结果**: `SubMind` 返回其当前的内心想法 (`current_mind`)。
+
+### 4.2. 规划阶段 (`_planner`)
+
+1. **输入**: 获取 `SubMind` 的当前想法 (`current_mind`)、`SubMind` 通过工具调用收集到的结构化信息 (`structured_info`) 以及观察到的最新消息。
+2. **构建提示词**: 调用 `_build_planner_prompt` 方法,将上述信息以及机器人个性、当前可用动作等整合进一个专门为规划器设计的提示词中。
+3. **定义动作工具**: 使用 `ActionManager.get_planner_tool_definition()` 获取当前可用动作(如 `no_reply`, `text_reply`, `emoji_reply`)的 JSON Schema,将其作为 "工具" 提供给 LLM。
+4. **调用 LLM**: 使用 `self.planner_llm` 向大模型发送请求,**强制要求**模型调用 `decide_reply_action` 这个"工具",并根据提示词内容决定使用哪个动作以及相应的参数(如 `reasoning`, `emoji_query`)。
+5. **处理 LLM 响应**: 使用 `process_llm_tool_response` 解析 LLM 返回的工具调用请求,提取出决策的动作 (`action`)、理由 (`reasoning`) 和可能的表情查询 (`emoji_query`)。
+6. **检查新消息与重新规划**:
+ - 调用 `_check_new_messages` 检查自规划阶段开始以来是否有新消息。
+ - 如果检测到新消息,有一定概率(当前为 30%)触发**重新规划**。这会再次调用 `_planner`,但会传入一个特殊的提示词片段(通过 `_build_replan_prompt` 生成),告知 LLM 它之前的决策以及现在需要重新考虑。
+7. **输出**: 返回一个包含最终决策结果(`action`, `reasoning`, `emoji_query` 等)的字典。如果 LLM 调用或解析失败,`action` 会被设为 "error"。
+
+### 4.3. 执行阶段 (`_handle_action`)
+
+根据规划阶段返回的 `action`,分派到不同的处理方法:
+
+- **`_handle_text_reply` (文本回复)**:
+ 1. `_get_anchor_message`: 获取一个用于回复的锚点消息。**注意**: 当前实现是创建一个系统触发的占位符消息作为锚点,而不是实际观察到的最后一条消息。
+ 2. `_create_thinking_message`: 调用 `HeartFCSender` 的 `register_thinking` 方法,标记机器人开始思考,并获取一个 `thinking_id`。
+ 3. `_replier_work`: 调用回复器生成回复内容。
+ 4. `_sender`: 调用发送器发送生成的文本和可能的表情。
+- **`_handle_emoji_reply` (仅表情回复)**:
+ 1. 获取锚点消息。
+ 2. `_handle_emoji`: 获取表情图片并调用 `HeartFCSender` 发送。
+- **`_handle_no_reply` (不回复)**:
+ 1. 记录不回复的理由。
+ 2. `_wait_for_new_message`: 进入等待状态,直到关联的 `Observation` 检测到新消息或超时(当前 300 秒)。
+
+## 5. 回复器逻辑 (`_replier_work`)
+
+- **输入**: 规划器给出的回复理由 (`reason`)、锚点消息 (`anchor_message`)、思考ID (`thinking_id`),以及通过 `self.sub_mind` 获取的结构化信息和当前想法。
+- **处理**: 调用 `self.gpt_instance` (`HeartFCGenerator`) 的 `generate_response` 方法。这个方法负责构建最终的生成提示词(结合思考、理由、上下文等),调用 LLM 生成回复文本。
+- **输出**: 返回一个包含多段回复文本的列表 (`List[str]`),如果生成失败则返回 `None`。
+
+## 6. 发送器逻辑 (`_sender`, `_create_thinking_message`, `_send_response_messages`, `_handle_emoji`)
+
+`HeartFChatting` 类本身不直接处理 WebSocket 发送,而是将发送任务委托给 `HeartFCSender` 实例 (`self.heart_fc_sender`)。
+
+- **`_create_thinking_message`**: 准备一个 `MessageThinking` 对象,并调用 `sender.register_thinking(thinking_message)`。
+- **`_send_response_messages`**:
+ - 检查对应的 `thinking_id` 是否仍然有效(通过 `sender.get_thinking_start_time`)。
+ - 遍历 `_replier_work` 返回的回复文本列表 (`response_set`)。
+ - 为每一段文本创建一个 `MessageSending` 对象。
+ - 调用 `sender.type_and_send_message(bot_message)` 来发送消息。`HeartFCSender` 内部会处理模拟打字延迟、实际发送和消息存储。
+ - 发送完成后,调用 `sender.complete_thinking(chat_id, thinking_id)` 来清理思考状态。
+ - 记录实际发送的消息 ID 到 `CycleInfo` 中。
+- **`_handle_emoji`**:
+ - 使用 `emoji_manager` 根据 `emoji_query` 获取表情图片路径。
+ - 将图片转为 Base64。
+ - 创建 `MessageSending` 对象(标记为 `is_emoji=True`)。
+ - 调用 `sender.send_and_store(bot_message)` 来发送并存储表情消息(这个方法不涉及思考状态)。
+
+## 7. 循环信息记录 (`CycleInfo`)
+
+- `CycleInfo` 类用于记录每一次思考-规划-执行循环的详细信息,包括:
+ - 循环 ID (`cycle_id`)
+ - 开始和结束时间 (`start_time`, `end_time`)
+ - 是否执行了实际动作 (`action_taken`)
+ - 决策的动作类型 (`action_type`) 和理由 (`reasoning`)
+ - 各阶段的耗时计时器 (`timers`)
+ - 关联的思考消息 ID (`thinking_id`)
+ - 是否发生了重新规划 (`replanned`)
+ - 详细的响应信息 (`response_info`),包括生成的文本、表情查询、锚点消息 ID、实际发送的消息 ID 列表以及 `SubMind` 的思考内容。
+- `HeartFChatting` 维护一个 `_cycle_history` 队列来保存最近的循环记录,方便调试和分析。
diff --git a/src/heart_flow/heartflow.py b/src/heart_flow/heartflow.py
index 7fbc0f58..7d92ae52 100644
--- a/src/heart_flow/heartflow.py
+++ b/src/heart_flow/heartflow.py
@@ -47,8 +47,8 @@ class Heartflow:
self.current_state: MaiStateInfo = MaiStateInfo() # 当前状态信息
self.mai_state_manager: MaiStateManager = MaiStateManager() # 状态决策管理器
- # 子心流管理
- self.subheartflow_manager: SubHeartflowManager = SubHeartflowManager() # 子心流管理器
+ # 子心流管理 (在初始化时传入 current_state)
+ self.subheartflow_manager: SubHeartflowManager = SubHeartflowManager(self.current_state)
# LLM模型配置
self.llm_model = LLMRequest(
@@ -75,23 +75,17 @@ class Heartflow:
inactive_threshold=INACTIVE_THRESHOLD_SECONDS,
)
- async def create_subheartflow(self, subheartflow_id: Any) -> Optional["SubHeartflow"]:
+ async def get_or_create_subheartflow(self, subheartflow_id: Any) -> Optional["SubHeartflow"]:
"""获取或创建一个新的SubHeartflow实例 - 委托给 SubHeartflowManager"""
- return await self.subheartflow_manager.create_or_get_subheartflow(subheartflow_id, self.current_state)
-
- def get_subheartflow(self, subheartflow_id: Any) -> Optional["SubHeartflow"]:
- """获取指定ID的SubHeartflow实例"""
- return self.subheartflow_manager.get_subheartflow(subheartflow_id)
-
- def get_all_subheartflows_streams_ids(self) -> list[Any]:
- """获取当前所有活跃的子心流的 ID 列表 - 委托给 SubHeartflowManager"""
- return self.subheartflow_manager.get_all_subheartflows_ids()
+ # 不再需要传入 self.current_state
+ return await self.subheartflow_manager.get_or_create_subheartflow(subheartflow_id)
async def heartflow_start_working(self):
"""启动后台任务"""
await self.background_task_manager.start_tasks()
logger.info("[Heartflow] 后台任务已启动")
+ # 根本不会用到这个函数吧,那样麦麦直接死了
async def stop_working(self):
"""停止所有任务和子心流"""
logger.info("[Heartflow] 正在停止任务和子心流...")
diff --git a/src/heart_flow/interest_logger.py b/src/heart_flow/interest_logger.py
index 05a7da39..7802f87b 100644
--- a/src/heart_flow/interest_logger.py
+++ b/src/heart_flow/interest_logger.py
@@ -54,11 +54,11 @@ class InterestLogger:
results = {}
if not all_flows:
- logger.debug("未找到任何子心流状态")
+ # logger.debug("未找到任何子心流状态")
return results
for subheartflow in all_flows:
- if self.subheartflow_manager.get_subheartflow(subheartflow.subheartflow_id):
+ if self.subheartflow_manager.get_or_create_subheartflow(subheartflow.subheartflow_id):
tasks.append(
asyncio.create_task(subheartflow.get_full_state(), name=f"get_state_{subheartflow.subheartflow_id}")
)
@@ -109,7 +109,7 @@ class InterestLogger:
}
if not all_subflow_states:
- logger.debug("没有获取到任何子心流状态,仅记录主心流状态")
+ # logger.debug("没有获取到任何子心流状态,仅记录主心流状态")
with open(self._history_log_file_path, "a", encoding="utf-8") as f:
f.write(json.dumps(log_entry_base, ensure_ascii=False) + "\n")
return
diff --git a/src/heart_flow/mai_state_manager.py b/src/heart_flow/mai_state_manager.py
index 740b715f..64fd4048 100644
--- a/src/heart_flow/mai_state_manager.py
+++ b/src/heart_flow/mai_state_manager.py
@@ -13,6 +13,7 @@ mai_state_config = LogConfig(
logger = get_module_logger("mai_state_manager", config=mai_state_config)
+# enable_unlimited_hfc_chat = True
enable_unlimited_hfc_chat = False
@@ -21,14 +22,14 @@ class MaiState(enum.Enum):
聊天状态:
OFFLINE: 不在线:回复概率极低,不会进行任何聊天
PEEKING: 看一眼手机:回复概率较低,会进行一些普通聊天
- NORMAL_CHAT: 正常聊天:回复概率较高,会进行一些普通聊天和少量的专注聊天
+ NORMAL_CHAT: 正常看手机:回复概率较高,会进行一些普通聊天和少量的专注聊天
FOCUSED_CHAT: 专注聊天:回复概率极高,会进行专注聊天和少量的普通聊天
"""
OFFLINE = "不在线"
- PEEKING = "看一眼"
- NORMAL_CHAT = "正常聊天"
- FOCUSED_CHAT = "专心聊天"
+ PEEKING = "看一眼手机"
+ NORMAL_CHAT = "正常看手机"
+ FOCUSED_CHAT = "专心看手机"
def get_normal_chat_max_num(self):
# 调试用
@@ -38,7 +39,7 @@ class MaiState(enum.Enum):
if self == MaiState.OFFLINE:
return 0
elif self == MaiState.PEEKING:
- return 1
+ return 2
elif self == MaiState.NORMAL_CHAT:
return 3
elif self == MaiState.FOCUSED_CHAT:
@@ -52,11 +53,11 @@ class MaiState(enum.Enum):
if self == MaiState.OFFLINE:
return 0
elif self == MaiState.PEEKING:
- return 0
+ return 1
elif self == MaiState.NORMAL_CHAT:
return 1
elif self == MaiState.FOCUSED_CHAT:
- return 2
+ return 3
class MaiStateInfo:
@@ -136,11 +137,11 @@ class MaiStateManager:
if current_status == MaiState.OFFLINE:
logger.info("当前[离线],没看手机,思考要不要上线看看......")
elif current_status == MaiState.PEEKING:
- logger.info("当前[看一眼],思考要不要继续聊下去......")
+ logger.info("当前[看一眼手机],思考要不要继续聊下去......")
elif current_status == MaiState.NORMAL_CHAT:
- logger.info("当前在[正常聊天]思考要不要继续聊下去......")
+ logger.info("当前在[正常看手机]思考要不要继续聊下去......")
elif current_status == MaiState.FOCUSED_CHAT:
- logger.info("当前在[专心聊天]思考要不要继续聊下去......")
+ logger.info("当前在[专心看手机]思考要不要继续聊下去......")
# 1. 麦麦每分钟都有概率离线
if time_since_last_min_check >= 60:
diff --git a/src/heart_flow/mind.py b/src/heart_flow/mind.py
index 6ca03c21..a40ee6ef 100644
--- a/src/heart_flow/mind.py
+++ b/src/heart_flow/mind.py
@@ -1,7 +1,7 @@
import traceback
from typing import TYPE_CHECKING
-from src.common.logger import get_module_logger
+from src.common.logger import get_module_logger, LogConfig, SUB_HEARTFLOW_MIND_STYLE_CONFIG
from src.plugins.models.utils_model import LLMRequest
from src.individuality.individuality import Individuality
from src.plugins.utils.prompt_builder import global_prompt_manager
@@ -12,7 +12,12 @@ if TYPE_CHECKING:
from src.heart_flow.subheartflow_manager import SubHeartflowManager
from src.heart_flow.mai_state_manager import MaiStateInfo
-logger = get_module_logger("mind")
+mind_log_config = LogConfig(
+ console_format=SUB_HEARTFLOW_MIND_STYLE_CONFIG["console_format"],
+ file_format=SUB_HEARTFLOW_MIND_STYLE_CONFIG["file_format"],
+)
+
+logger = get_module_logger("mind", config=mind_log_config)
class Mind:
@@ -22,9 +27,6 @@ class Mind:
self.subheartflow_manager = subheartflow_manager
self.llm_model = llm_model
self.individuality = Individuality.get_instance()
- # Main mind state is still managed by Heartflow for now
- # self.current_mind = "你什么也没想"
- # self.past_mind = []
async def do_a_thinking(self, current_main_mind: str, mai_state_info: "MaiStateInfo", schedule_info: str):
"""
diff --git a/src/heart_flow/observation.py b/src/heart_flow/observation.py
index ba4d23de..b960154c 100644
--- a/src/heart_flow/observation.py
+++ b/src/heart_flow/observation.py
@@ -23,6 +23,9 @@ class Observation:
self.observe_id = observe_id
self.last_observe_time = datetime.now().timestamp() # 初始化为当前时间
+ async def observe(self):
+ pass
+
# 聊天观察
class ChattingObservation(Observation):
@@ -78,15 +81,17 @@ class ChattingObservation(Observation):
return self.talking_message_str
async def observe(self):
+ # 自上一次观察的新消息
new_messages_list = get_raw_msg_by_timestamp_with_chat(
chat_id=self.chat_id,
timestamp_start=self.last_observe_time,
- timestamp_end=datetime.now().timestamp(), # 使用当前时间作为结束时间戳
+ timestamp_end=datetime.now().timestamp(),
limit=self.max_now_obs_len,
limit_mode="latest",
)
- if new_messages_list: # 检查列表是否为空
- last_obs_time_mark = self.last_observe_time
+
+ last_obs_time_mark = self.last_observe_time
+ if new_messages_list:
self.last_observe_time = new_messages_list[-1]["time"]
self.talking_message.extend(new_messages_list)
@@ -97,9 +102,7 @@ class ChattingObservation(Observation):
self.talking_message = self.talking_message[messages_to_remove_count:] # 保留后半部分,即最新的
oldest_messages_str = await build_readable_messages(
- messages=oldest_messages,
- timestamp_mode="normal",
- read_mark=last_obs_time_mark,
+ messages=oldest_messages, timestamp_mode="normal", read_mark=0
)
# 调用 LLM 总结主题
@@ -137,7 +140,11 @@ class ChattingObservation(Observation):
)
self.mid_memory_info = mid_memory_str
- self.talking_message_str = await build_readable_messages(messages=self.talking_message, timestamp_mode="normal")
+ self.talking_message_str = await build_readable_messages(
+ messages=self.talking_message,
+ timestamp_mode="normal",
+ read_mark=last_obs_time_mark,
+ )
logger.trace(
f"Chat {self.chat_id} - 压缩早期记忆:{self.mid_memory_info}\n现在聊天内容:{self.talking_message_str}"
diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py
index 76d60b14..9cbd7b3a 100644
--- a/src/heart_flow/sub_heartflow.py
+++ b/src/heart_flow/sub_heartflow.py
@@ -1,26 +1,19 @@
from .observation import Observation, ChattingObservation
import asyncio
-from src.plugins.moods.moods import MoodManager
-from src.plugins.models.utils_model import LLMRequest
from src.config.config import global_config
import time
-from typing import Optional, List, Dict, Callable
+from typing import Optional, List, Dict, Tuple
import traceback
-from src.plugins.chat.utils import parse_text_timestamps
-import enum
from src.common.logger import get_module_logger, LogConfig, SUB_HEARTFLOW_STYLE_CONFIG # noqa: E402
-from src.individuality.individuality import Individuality
import random
-from src.plugins.person_info.relationship_manager import relationship_manager
-from ..plugins.utils.prompt_builder import Prompt, global_prompt_manager
from src.plugins.chat.message import MessageRecv
from src.plugins.chat.chat_stream import chat_manager
import math
from src.plugins.heartFC_chat.heartFC_chat import HeartFChatting
from src.plugins.heartFC_chat.normal_chat import NormalChat
-
-# from src.do_tool.tool_use import ToolUser
from src.heart_flow.mai_state_manager import MaiStateInfo
+from src.heart_flow.chat_state_info import ChatState, ChatStateInfo
+from src.heart_flow.sub_mind import SubMind
# 定义常量 (从 interest.py 移动过来)
@@ -33,48 +26,6 @@ subheartflow_config = LogConfig(
)
logger = get_module_logger("subheartflow", config=subheartflow_config)
-interest_log_config = LogConfig(
- console_format=SUB_HEARTFLOW_STYLE_CONFIG["console_format"],
- file_format=SUB_HEARTFLOW_STYLE_CONFIG["file_format"],
-)
-interest_logger = get_module_logger("InterestChatting", config=interest_log_config)
-
-
-def init_prompt():
- prompt = ""
- # prompt += f"麦麦的总体想法是:{self.main_heartflow_info}\n\n"
- prompt += "{extra_info}\n"
- # prompt += "{prompt_schedule}\n"
- # prompt += "{relation_prompt_all}\n"
- prompt += "{prompt_personality}\n"
- prompt += "刚刚你的想法是:\n我是{bot_name},我想,{current_thinking_info}\n"
- prompt += "-----------------------------------\n"
- prompt += "现在是{time_now},你正在上网,和qq群里的网友们聊天,群里正在聊的话题是:\n{chat_observe_info}\n"
- prompt += "\n你现在{mood_info}\n"
- # prompt += "你注意到{sender_name}刚刚说:{message_txt}\n"
- prompt += "现在请你根据刚刚的想法继续思考,思考时可以想想如何对群聊内容进行回复,要不要对群里的话题进行回复,关注新话题,可以适当转换话题,大家正在说的话才是聊天的主题。\n"
- prompt += "回复的要求是:平淡一些,简短一些,说中文,如果你要回复,最好只回复一个人的一个话题\n"
- prompt += "请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要带有括号和动作描写。不要回复自己的发言,尽量不要说你说过的话。"
- prompt += "现在请你{hf_do_next},不要分点输出,生成内心想法,文字不要浮夸"
-
- Prompt(prompt, "sub_heartflow_prompt_before")
-
-
-class ChatState(enum.Enum):
- ABSENT = "没在看群"
- CHAT = "随便水群"
- FOCUSED = "激情水群"
-
-
-class ChatStateInfo:
- def __init__(self):
- self.chat_status: ChatState = ChatState.ABSENT
- self.current_state_time = 120
-
- self.mood_manager = MoodManager()
- self.mood = self.mood_manager.get_prompt()
-
-
base_reply_probability = 0.05
probability_increase_rate_per_second = 0.08
max_reply_probability = 1
@@ -90,8 +41,8 @@ class InterestChatting:
increase_rate=probability_increase_rate_per_second,
decay_factor=global_config.probability_decay_factor_per_second,
max_probability=max_reply_probability,
- state_change_callback: Optional[Callable[[ChatState], None]] = None,
):
+ # 基础属性初始化
self.interest_level: float = 0.0
self.last_update_time: float = time.time()
self.decay_rate_per_second: float = decay_rate
@@ -105,16 +56,35 @@ class InterestChatting:
self.max_reply_probability: float = max_probability
self.current_reply_probability: float = 0.0
self.is_above_threshold: bool = False
+
+ # 任务相关属性初始化
self.update_task: Optional[asyncio.Task] = None
self._stop_event = asyncio.Event()
+ self._task_lock = asyncio.Lock()
+ self._is_running = False
self.interest_dict: Dict[str, tuple[MessageRecv, float, bool]] = {}
self.update_interval = 1.0
- self.start_updates(self.update_interval) # 初始化时启动后台更新任务
self.above_threshold = False
self.start_hfc_probability = 0.0
+ async def initialize(self):
+ async with self._task_lock:
+ if self._is_running:
+ logger.debug("后台兴趣更新任务已在运行中。")
+ return
+
+ # 清理已完成或已取消的任务
+ if self.update_task and (self.update_task.done() or self.update_task.cancelled()):
+ self.update_task = None
+
+ if not self.update_task:
+ self._stop_event.clear()
+ self._is_running = True
+ self.update_task = asyncio.create_task(self._run_update_loop(self.update_interval))
+ logger.debug("后台兴趣更新任务已创建并启动。")
+
def add_interest_dict(self, message: MessageRecv, interest_value: float, is_mentioned: bool):
self.interest_dict[message.message_info.message_id] = (message, interest_value, is_mentioned)
self.last_interaction_time = time.time()
@@ -139,7 +109,7 @@ class InterestChatting:
# 异常情况处理
if self.decay_rate_per_second <= 0:
- interest_logger.warning(f"衰减率({self.decay_rate_per_second})无效,重置兴趣值为0")
+ logger.warning(f"衰减率({self.decay_rate_per_second})无效,重置兴趣值为0")
self.interest_level = 0.0
return
@@ -148,7 +118,7 @@ class InterestChatting:
decay_factor = math.pow(self.decay_rate_per_second, self.update_interval)
self.interest_level *= decay_factor
except ValueError as e:
- interest_logger.error(
+ logger.error(
f"衰减计算错误: {e} 参数: 衰减率={self.decay_rate_per_second} 时间差={self.update_interval} 当前兴趣={self.interest_level}"
)
self.interest_level = 0.0
@@ -161,11 +131,11 @@ class InterestChatting:
if self.start_hfc_probability != 0:
self.start_hfc_probability -= 0.1
- async def increase_interest(self, current_time: float, value: float):
+ async def increase_interest(self, value: float):
self.interest_level += value
self.interest_level = min(self.interest_level, self.max_interest)
- async def decrease_interest(self, current_time: float, value: float):
+ async def decrease_interest(self, value: float):
self.interest_level -= value
self.interest_level = max(self.interest_level, 0.0)
@@ -190,59 +160,57 @@ class InterestChatting:
# --- 新增后台更新任务相关方法 ---
async def _run_update_loop(self, update_interval: float = 1.0):
"""后台循环,定期更新兴趣和回复概率。"""
- while not self._stop_event.is_set():
- try:
- if self.interest_level != 0:
- await self._calculate_decay()
+ try:
+ while not self._stop_event.is_set():
+ try:
+ if self.interest_level != 0:
+ await self._calculate_decay()
- await self._update_reply_probability()
+ await self._update_reply_probability()
- # 等待下一个周期或停止事件
- await asyncio.wait_for(self._stop_event.wait(), timeout=update_interval)
- except asyncio.TimeoutError:
- # 正常超时,继续循环
- continue
- except asyncio.CancelledError:
- interest_logger.info("InterestChatting 更新循环被取消。")
- break
- except Exception as e:
- interest_logger.error(f"InterestChatting 更新循环出错: {e}")
- interest_logger.error(traceback.format_exc())
- # 防止错误导致CPU飙升,稍作等待
- await asyncio.sleep(5)
- interest_logger.info("InterestChatting 更新循环已停止。")
-
- def start_updates(self, update_interval: float = 1.0):
- """启动后台更新任务"""
- if self.update_task is None or self.update_task.done():
- self._stop_event.clear()
- self.update_task = asyncio.create_task(self._run_update_loop(update_interval))
- interest_logger.debug("后台兴趣更新任务已创建并启动。")
- else:
- interest_logger.debug("后台兴趣更新任务已在运行中。")
+ # 等待下一个周期或停止事件
+ await asyncio.wait_for(self._stop_event.wait(), timeout=update_interval)
+ except asyncio.TimeoutError:
+ # 正常超时,继续循环
+ continue
+ except Exception as e:
+ logger.error(f"InterestChatting 更新循环出错: {e}")
+ logger.error(traceback.format_exc())
+ # 防止错误导致CPU飙升,稍作等待
+ await asyncio.sleep(5)
+ except asyncio.CancelledError:
+ logger.info("InterestChatting 更新循环被取消。")
+ finally:
+ self._is_running = False
+ logger.info("InterestChatting 更新循环已停止。")
async def stop_updates(self):
- """停止后台更新任务"""
- if self.update_task and not self.update_task.done():
- interest_logger.info("正在停止 InterestChatting 后台更新任务...")
- self._stop_event.set() # 发送停止信号
- try:
- # 等待任务结束,设置超时
- await asyncio.wait_for(self.update_task, timeout=5.0)
- interest_logger.info("InterestChatting 后台更新任务已成功停止。")
- except asyncio.TimeoutError:
- interest_logger.warning("停止 InterestChatting 后台任务超时,尝试取消...")
- self.update_task.cancel()
+ """停止后台更新任务,使用锁确保并发安全"""
+ async with self._task_lock:
+ if not self._is_running:
+ logger.debug("后台兴趣更新任务未运行。")
+ return
+
+ logger.info("正在停止 InterestChatting 后台更新任务...")
+ self._stop_event.set()
+
+ if self.update_task and not self.update_task.done():
try:
- await self.update_task # 等待取消完成
- except asyncio.CancelledError:
- interest_logger.info("InterestChatting 后台更新任务已被取消。")
- except Exception as e:
- interest_logger.error(f"停止 InterestChatting 后台任务时发生异常: {e}")
- finally:
- self.update_task = None
- else:
- interest_logger.debug("InterestChatting 后台更新任务未运行或已完成。")
+ # 等待任务结束,设置超时
+ await asyncio.wait_for(self.update_task, timeout=5.0)
+ logger.info("InterestChatting 后台更新任务已成功停止。")
+ except asyncio.TimeoutError:
+ logger.warning("停止 InterestChatting 后台任务超时,尝试取消...")
+ self.update_task.cancel()
+ try:
+ await self.update_task # 等待取消完成
+ except asyncio.CancelledError:
+ logger.info("InterestChatting 后台更新任务已被取消。")
+ except Exception as e:
+ logger.error(f"停止 InterestChatting 后台任务时发生异常: {e}")
+ finally:
+ self.update_task = None
+ self._is_running = False
# --- 结束 新增方法 ---
@@ -255,58 +223,59 @@ class SubHeartflow:
subheartflow_id: 子心流唯一标识符
parent_heartflow: 父级心流实例
"""
- # 基础属性
+ # 基础属性,两个值是一样的
self.subheartflow_id = subheartflow_id
self.chat_id = subheartflow_id
+ # 麦麦的状态
self.mai_states = mai_states
- # 思维状态相关
- self.current_mind = "什么也没想" # 当前想法
- self.past_mind = [] # 历史想法记录
+ # 这个聊天流的状态
+ self.chat_state: ChatStateInfo = ChatStateInfo()
+ self.chat_state_changed_time: float = time.time()
+ self.chat_state_last_time: float = 0
+ self.history_chat_state: List[Tuple[ChatState, float]] = []
- # 聊天状态管理
- self.chat_state: ChatStateInfo = ChatStateInfo() # 该sub_heartflow的聊天状态信息
- self.interest_chatting = InterestChatting(
- state_change_callback=self.set_chat_state
- ) # 该sub_heartflow的兴趣系统
+ # 兴趣检测器
+ self.interest_chatting: InterestChatting = InterestChatting()
# 活动状态管理
- self.last_active_time = time.time() # 最后活跃时间
self.should_stop = False # 停止标志
self.task: Optional[asyncio.Task] = None # 后台任务
+
+ # 随便水群 normal_chat 和 认真水群 heartFC_chat 实例
+ # CHAT模式激活 随便水群 FOCUS模式激活 认真水群
self.heart_fc_instance: Optional[HeartFChatting] = None # 该sub_heartflow的HeartFChatting实例
self.normal_chat_instance: Optional[NormalChat] = None # 该sub_heartflow的NormalChat实例
- # 观察和知识系统
+ # 观察,目前只有聊天观察,可以载入多个
+ # 负责对处理过的消息进行观察
self.observations: List[ChattingObservation] = [] # 观察列表
- self.running_knowledges = [] # 运行中的知识
+ # self.running_knowledges = [] # 运行中的知识,待完善
- # LLM模型配置
- self.llm_model = LLMRequest(
- model=global_config.llm_sub_heartflow,
- temperature=global_config.llm_sub_heartflow["temp"],
- max_tokens=800,
- request_type="sub_heart_flow",
+ # LLM模型配置,负责进行思考
+ self.sub_mind = SubMind(
+ subheartflow_id=self.subheartflow_id, chat_state=self.chat_state, observations=self.observations
)
+ # 日志前缀
self.log_prefix = chat_manager.get_stream_name(self.subheartflow_id) or self.subheartflow_id
- async def add_time_current_state(self, add_time: float):
- self.current_state_time += add_time
+ async def initialize(self):
+ """异步初始化方法,创建兴趣流"""
+ await self.interest_chatting.initialize()
+ logger.debug(f"{self.log_prefix} InterestChatting 实例已初始化。")
- async def change_to_state_chat(self):
- self.current_state_time = 120
- self._start_normal_chat()
-
- async def change_to_state_focused(self):
- self.current_state_time = 60
- self._start_heart_fc_chat()
+ def update_last_chat_state_time(self):
+ self.chat_state_last_time = time.time() - self.chat_state_changed_time
async def _stop_normal_chat(self):
- """停止 NormalChat 的兴趣监控"""
+ """
+ 停止 NormalChat 实例
+ 切出 CHAT 状态时使用
+ """
if self.normal_chat_instance:
- logger.info(f"{self.log_prefix} 停止 NormalChat 兴趣监控...")
+ logger.info(f"{self.log_prefix} 离开CHAT模式,结束 随便水群")
try:
await self.normal_chat_instance.stop_chat() # 调用 stop_chat
except Exception as e:
@@ -314,23 +283,21 @@ class SubHeartflow:
logger.error(traceback.format_exc())
async def _start_normal_chat(self) -> bool:
- """启动 NormalChat 实例及其兴趣监控,确保 HeartFChatting 已停止"""
- await self._stop_heart_fc_chat() # 确保专注聊天已停止
+ """
+ 启动 NormalChat 实例,
+ 进入 CHAT 状态时使用
+
+ 确保 HeartFChatting 已停止
+ """
+ await self._stop_heart_fc_chat() # 确保 专注聊天已停止
log_prefix = self.log_prefix
try:
- # 总是尝试创建或获取最新的 stream 和 interest_dict
+ # 获取聊天流并创建 NormalChat 实例
chat_stream = chat_manager.get_stream(self.chat_id)
- if not chat_stream:
- logger.error(f"{log_prefix} 无法获取 chat_stream,无法启动 NormalChat。")
- return False
-
- # 如果实例不存在或需要更新,则创建新实例
- # if not self.normal_chat_instance: # 或者总是重新创建以获取最新的 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} 启动 NormalChat 兴趣监控...")
+ logger.info(f"{log_prefix} 启动 NormalChat 随便水群...")
await self.normal_chat_instance.start_chat() # <--- 修正:调用 start_chat
return True
except Exception as e:
@@ -380,7 +347,7 @@ class SubHeartflow:
logger.info(f"{log_prefix} 麦麦准备开始专注聊天 (创建新实例)...")
try:
self.heart_fc_instance = HeartFChatting(
- chat_id=self.chat_id,
+ chat_id=self.chat_id, sub_mind=self.sub_mind, observations=self.observations
)
if await self.heart_fc_instance._initialize():
await self.heart_fc_instance.start() # 初始化成功后启动循环
@@ -396,55 +363,38 @@ class SubHeartflow:
self.heart_fc_instance = None # 创建或初始化异常,清理实例
return False
- async def set_chat_state(self, new_state: "ChatState", current_states_num: tuple = ()):
+ async def change_chat_state(self, new_state: "ChatState"):
"""更新sub_heartflow的聊天状态,并管理 HeartFChatting 和 NormalChat 实例及任务"""
current_state = self.chat_state.chat_status
+
if current_state == new_state:
- # logger.trace(f"{self.log_prefix} 状态已为 {current_state.value}, 无需更改。") # 减少日志噪音
return
log_prefix = self.log_prefix
- current_mai_state = self.mai_states.get_current_state()
state_changed = False # 标记状态是否实际发生改变
# --- 状态转换逻辑 ---
if new_state == ChatState.CHAT:
- normal_limit = current_mai_state.get_normal_chat_max_num()
- current_chat_count = current_states_num[1] if len(current_states_num) > 1 else 0
-
- if current_chat_count >= normal_limit and current_state != ChatState.CHAT:
- logger.debug(
- f"{log_prefix} 无法从 {current_state.value} 转到 聊天。原因:聊不过来了 ({current_chat_count}/{normal_limit})"
- )
- return # 阻止状态转换
+ # 移除限额检查逻辑
+ logger.debug(f"{log_prefix} 准备进入或保持 聊天 状态")
+ if await self._start_normal_chat():
+ logger.info(f"{log_prefix} 成功进入或保持 NormalChat 状态。")
+ state_changed = True
else:
- logger.debug(f"{log_prefix} 准备进入或保持 聊天 状态 ({current_chat_count}/{normal_limit})")
- if await self._start_normal_chat():
- logger.info(f"{log_prefix} 成功进入或保持 NormalChat 状态。")
- state_changed = True
- else:
- logger.error(f"{log_prefix} 启动 NormalChat 失败,无法进入 CHAT 状态。")
- # 考虑是否需要回滚状态或采取其他措施
- return # 启动失败,不改变状态
+ logger.error(f"{log_prefix} 启动 NormalChat 失败,无法进入 CHAT 状态。")
+ # 考虑是否需要回滚状态或采取其他措施
+ return # 启动失败,不改变状态
elif new_state == ChatState.FOCUSED:
- focused_limit = current_mai_state.get_focused_chat_max_num()
- current_focused_count = current_states_num[2] if len(current_states_num) > 2 else 0
-
- if current_focused_count >= focused_limit and current_state != ChatState.FOCUSED:
- logger.debug(
- f"{log_prefix} 无法从 {current_state.value} 转到 专注。原因:聊不过来了 ({current_focused_count}/{focused_limit})"
- )
- return # 阻止状态转换
+ # 移除限额检查逻辑
+ logger.debug(f"{log_prefix} 准备进入或保持 专注聊天 状态")
+ if await self._start_heart_fc_chat():
+ logger.info(f"{log_prefix} 成功进入或保持 HeartFChatting 状态。")
+ state_changed = True
else:
- logger.debug(f"{log_prefix} 准备进入或保持 专注聊天 状态 ({current_focused_count}/{focused_limit})")
- if await self._start_heart_fc_chat():
- logger.info(f"{log_prefix} 成功进入或保持 HeartFChatting 状态。")
- state_changed = True
- else:
- logger.error(f"{log_prefix} 启动 HeartFChatting 失败,无法进入 FOCUSED 状态。")
- # 启动失败,状态回滚到之前的状态或ABSENT?这里保持不改变
- return # 启动失败,不改变状态
+ logger.error(f"{log_prefix} 启动 HeartFChatting 失败,无法进入 FOCUSED 状态。")
+ # 启动失败,状态回滚到之前的状态或ABSENT?这里保持不改变
+ return # 启动失败,不改变状态
elif new_state == ChatState.ABSENT:
logger.info(f"{log_prefix} 进入 ABSENT 状态,停止所有聊天活动...")
@@ -454,9 +404,16 @@ class SubHeartflow:
# --- 更新状态和最后活动时间 ---
if state_changed:
- logger.info(f"{log_prefix} 麦麦的聊天状态从 {current_state.value} 变更为 {new_state.value}")
+ self.update_last_chat_state_time()
+ self.history_chat_state.append((current_state, self.chat_state_last_time))
+
+ logger.info(
+ f"{log_prefix} 麦麦的聊天状态从 {current_state.value} (持续了 {self.chat_state_last_time} 秒) 变更为 {new_state.value}"
+ )
+
self.chat_state.chat_status = new_state
- self.last_active_time = time.time()
+ self.chat_state_last_time = 0
+ self.chat_state_changed_time = time.time()
else:
# 如果因为某些原因(如启动失败)没有成功改变状态,记录一下
logger.debug(
@@ -470,111 +427,15 @@ class SubHeartflow:
- 负责子心流的主要后台循环
- 每30秒检查一次停止标志
"""
- logger.info(f"{self.log_prefix} 子心流开始工作...")
+ logger.trace(f"{self.log_prefix} 子心流开始工作...")
while not self.should_stop:
await asyncio.sleep(30) # 30秒检查一次停止标志
logger.info(f"{self.log_prefix} 子心流后台任务已停止。")
- async def do_thinking_before_reply(
- self,
- extra_info: str,
- obs_id: list[str] = None,
- ):
- self.last_active_time = time.time()
-
- current_thinking_info = self.current_mind
- mood_info = self.chat_state.mood
- observation = self._get_primary_observation()
-
- chat_observe_info = ""
- if obs_id:
- try:
- chat_observe_info = observation.get_observe_info(obs_id)
- logger.debug(f"[{self.subheartflow_id}] Using specific observation IDs: {obs_id}")
- except Exception as e:
- logger.error(
- f"[{self.subheartflow_id}] Error getting observe info with IDs {obs_id}: {e}. Falling back."
- )
- chat_observe_info = observation.get_observe_info()
- else:
- chat_observe_info = observation.get_observe_info()
- # logger.debug(f"[{self.subheartflow_id}] Using default observation info.")
-
- extra_info_prompt = ""
- if extra_info:
- for tool_name, tool_data in extra_info.items():
- extra_info_prompt += f"{tool_name} 相关信息:\n"
- for item in tool_data:
- extra_info_prompt += f"- {item['name']}: {item['content']}\n"
- else:
- extra_info_prompt = "无工具信息。\n"
-
- individuality = Individuality.get_instance()
- 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}"
-
- time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
-
- local_random = random.Random()
- current_minute = int(time.strftime("%M"))
- local_random.seed(current_minute)
-
- hf_options = [
- ("继续生成你在这个聊天中的想法,在原来想法的基础上继续思考", 0.7),
- ("生成你在这个聊天中的想法,在原来的想法上尝试新的话题", 0.1),
- ("生成你在这个聊天中的想法,不要太深入", 0.1),
- ("继续生成你在这个聊天中的想法,进行深入思考", 0.1),
- ]
-
- hf_do_next = local_random.choices(
- [option[0] for option in hf_options], weights=[option[1] for option in hf_options], k=1
- )[0]
-
- prompt = (await global_prompt_manager.get_prompt_async("sub_heartflow_prompt_before")).format(
- extra_info=extra_info_prompt,
- prompt_personality=prompt_personality,
- bot_name=individuality.personality.bot_nickname,
- current_thinking_info=current_thinking_info,
- time_now=time_now,
- chat_observe_info=chat_observe_info,
- mood_info=mood_info,
- hf_do_next=hf_do_next,
- )
-
- prompt = await relationship_manager.convert_all_person_sign_to_person_name(prompt)
- prompt = parse_text_timestamps(prompt, mode="lite")
-
- logger.debug(f"[{self.subheartflow_id}] 心流思考prompt:\n{prompt}\n")
-
- try:
- response, reasoning_content = await self.llm_model.generate_response_async(prompt)
-
- logger.debug(f"[{self.subheartflow_id}] 心流思考结果:\n{response}\n")
-
- if not response:
- response = "(不知道该想些什么...)"
- logger.warning(f"[{self.subheartflow_id}] LLM 返回空结果,思考失败。")
- except Exception as e:
- logger.error(f"[{self.subheartflow_id}] 内心独白获取失败: {e}")
- response = "(思考时发生错误...)"
-
- self.update_current_mind(response)
-
- return self.current_mind, self.past_mind
-
def update_current_mind(self, response):
- self.past_mind.append(self.current_mind)
- self.current_mind = response
+ self.sub_mind.update_current_mind(response)
def add_observation(self, observation: Observation):
for existing_obs in self.observations:
@@ -607,9 +468,6 @@ class SubHeartflow:
async def should_evaluate_reply(self) -> bool:
return await self.interest_chatting.should_evaluate_reply()
- async def add_interest_dict_entry(self, message: MessageRecv, interest_value: float, is_mentioned: bool):
- self.interest_chatting.add_interest_dict(message, interest_value, is_mentioned)
-
def get_interest_dict(self) -> Dict[str, tuple[MessageRecv, float, bool]]:
return self.interest_chatting.interest_dict
@@ -621,9 +479,9 @@ class SubHeartflow:
interest_state = await self.get_interest_state()
return {
"interest_state": interest_state,
- "current_mind": self.current_mind,
+ "current_mind": self.sub_mind.current_mind,
"chat_state": self.chat_state.chat_status.value,
- "last_active_time": self.last_active_time,
+ "last_changed_state_time": self.last_changed_state_time,
}
async def shutdown(self):
@@ -661,6 +519,3 @@ class SubHeartflow:
self.chat_state.chat_status = ChatState.ABSENT # 状态重置为不参与
logger.info(f"{self.log_prefix} 子心流关闭完成。")
-
-
-init_prompt()
diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py
new file mode 100644
index 00000000..be995b84
--- /dev/null
+++ b/src/heart_flow/sub_mind.py
@@ -0,0 +1,278 @@
+from .observation import Observation
+from src.plugins.models.utils_model import LLMRequest
+from src.config.config import global_config
+import time
+import traceback
+from src.common.logger import get_module_logger, LogConfig, SUB_HEARTFLOW_STYLE_CONFIG # noqa: E402
+from src.individuality.individuality import Individuality
+import random
+from ..plugins.utils.prompt_builder import Prompt, global_prompt_manager
+from src.do_tool.tool_use import ToolUser
+from src.plugins.utils.json_utils import safe_json_dumps, normalize_llm_response, process_llm_tool_calls
+from src.heart_flow.chat_state_info import ChatStateInfo
+from src.plugins.chat.chat_stream import chat_manager
+from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo
+
+subheartflow_config = LogConfig(
+ console_format=SUB_HEARTFLOW_STYLE_CONFIG["console_format"],
+ file_format=SUB_HEARTFLOW_STYLE_CONFIG["file_format"],
+)
+logger = get_module_logger("subheartflow", config=subheartflow_config)
+
+
+def init_prompt():
+ prompt = ""
+ prompt += "{extra_info}\n"
+ prompt += "{prompt_personality}\n"
+ prompt += "{last_loop_prompt}\n"
+ prompt += "现在是{time_now},你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容:\n{chat_observe_info}\n"
+ prompt += "\n你现在{mood_info}\n"
+ prompt += (
+ "请仔细阅读当前群聊内容,分析讨论话题和群成员关系,分析你刚刚发言和别人对你的发言的反应,思考你要不要回复。"
+ )
+ prompt += "思考并输出你的内心想法\n"
+ prompt += "输出要求:\n"
+ prompt += "1. 根据聊天内容生成你的想法,{hf_do_next}\n"
+ prompt += "2. 不要分点、不要使用表情符号\n"
+ prompt += "3. 避免多余符号(冒号、引号、括号等)\n"
+ prompt += "4. 语言简洁自然,不要浮夸\n"
+ prompt += "5. 如果你刚发言,并且没有人回复你,不要回复\n"
+ prompt += "工具使用说明:\n"
+ prompt += "1. 输出想法后考虑是否需要使用工具\n"
+ prompt += "2. 工具可获取信息或执行操作\n"
+ prompt += "3. 如需处理消息或回复,请使用工具\n"
+
+ Prompt(prompt, "sub_heartflow_prompt_before")
+
+ prompt = ""
+ prompt += "刚刚你的内心想法是:{current_thinking_info}\n"
+ prompt += "{if_replan_prompt}\n"
+
+ Prompt(prompt, "last_loop")
+
+
+class SubMind:
+ def __init__(self, subheartflow_id: str, chat_state: ChatStateInfo, observations: Observation):
+ self.subheartflow_id = subheartflow_id
+
+ self.llm_model = LLMRequest(
+ model=global_config.llm_sub_heartflow,
+ temperature=global_config.llm_sub_heartflow["temp"],
+ max_tokens=800,
+ request_type="sub_heart_flow",
+ )
+
+ self.chat_state = chat_state
+ self.observations = observations
+
+ self.current_mind = ""
+ self.past_mind = []
+ self.structured_info = {}
+
+ async def do_thinking_before_reply(self, last_cycle: CycleInfo = None):
+ """
+ 在回复前进行思考,生成内心想法并收集工具调用结果
+
+ 返回:
+ tuple: (current_mind, past_mind) 当前想法和过去的想法列表
+ """
+ # 更新活跃时间
+ self.last_active_time = time.time()
+
+ # ---------- 1. 准备基础数据 ----------
+ # 获取现有想法和情绪状态
+ current_thinking_info = self.current_mind
+ mood_info = self.chat_state.mood
+
+ # 获取观察对象
+ observation = self.observations[0]
+ if not observation:
+ logger.error(f"[{self.subheartflow_id}] 无法获取观察对象")
+ self.update_current_mind("(我没看到任何聊天内容...)")
+ return self.current_mind, self.past_mind
+
+ # 获取观察内容
+ chat_observe_info = observation.get_observe_info()
+
+ # ---------- 2. 准备工具和个性化数据 ----------
+ # 初始化工具
+ tool_instance = ToolUser()
+ tools = tool_instance._define_tools()
+
+ # 获取个性化信息
+ individuality = Individuality.get_instance()
+
+ # 构建个性部分
+ 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}"
+
+ # 获取当前时间
+ time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
+
+ # ---------- 3. 构建思考指导部分 ----------
+ # 创建本地随机数生成器,基于分钟数作为种子
+ local_random = random.Random()
+ current_minute = int(time.strftime("%M"))
+ local_random.seed(current_minute)
+
+ # 思考指导选项和权重
+ hf_options = [
+ ("可以参考之前的想法,在原来想法的基础上继续思考", 0.2),
+ ("可以参考之前的想法,在原来的想法上尝试新的话题", 0.4),
+ ("不要太深入", 0.2),
+ ("进行深入思考", 0.2),
+ ]
+
+ # 上一次决策信息
+ if last_cycle != None:
+ last_action = last_cycle.action_type
+ last_reasoning = last_cycle.reasoning
+ is_replan = last_cycle.replanned
+ if is_replan:
+ if_replan_prompt = f"但是你有了上述想法之后,有了新消息,你决定重新思考后,你做了:{last_action}\n因为:{last_reasoning}\n"
+ else:
+ if_replan_prompt = f"出于这个想法,你刚才做了:{last_action}\n因为:{last_reasoning}\n"
+ else:
+ last_action = ""
+ last_reasoning = ""
+ is_replan = False
+ if_replan_prompt = ""
+ if current_thinking_info:
+ last_loop_prompt = (await global_prompt_manager.get_prompt_async("last_loop")).format(
+ current_thinking_info=current_thinking_info, if_replan_prompt=if_replan_prompt
+ )
+ else:
+ last_loop_prompt = ""
+
+ # 加权随机选择思考指导
+ hf_do_next = local_random.choices(
+ [option[0] for option in hf_options], weights=[option[1] for option in hf_options], k=1
+ )[0]
+
+ # ---------- 4. 构建最终提示词 ----------
+ # 获取提示词模板并填充数据
+ prompt = (await global_prompt_manager.get_prompt_async("sub_heartflow_prompt_before")).format(
+ extra_info="", # 可以在这里添加额外信息
+ prompt_personality=prompt_personality,
+ bot_name=individuality.personality.bot_nickname,
+ time_now=time_now,
+ chat_observe_info=chat_observe_info,
+ mood_info=mood_info,
+ hf_do_next=hf_do_next,
+ last_loop_prompt=last_loop_prompt,
+ )
+
+ # logger.debug(f"[{self.subheartflow_id}] 心流思考提示词构建完成")
+
+ # ---------- 5. 执行LLM请求并处理响应 ----------
+ content = "" # 初始化内容变量
+ _reasoning_content = "" # 初始化推理内容变量
+
+ try:
+ # 调用LLM生成响应
+ response = await self.llm_model.generate_response_tool_async(prompt=prompt, tools=tools)
+
+ # 标准化响应格式
+ success, normalized_response, error_msg = normalize_llm_response(
+ response, log_prefix=f"[{self.subheartflow_id}] "
+ )
+
+ if not success:
+ # 处理标准化失败情况
+ logger.warning(f"[{self.subheartflow_id}] {error_msg}")
+ content = "LLM响应格式无法处理"
+ else:
+ # 从标准化响应中提取内容
+ if len(normalized_response) >= 2:
+ content = normalized_response[0]
+ _reasoning_content = normalized_response[1] if len(normalized_response) > 1 else ""
+
+ # 处理可能的工具调用
+ if len(normalized_response) == 3:
+ # 提取并验证工具调用
+ success, valid_tool_calls, error_msg = process_llm_tool_calls(
+ normalized_response, log_prefix=f"[{self.subheartflow_id}] "
+ )
+
+ if success and valid_tool_calls:
+ # 记录工具调用信息
+ tool_calls_str = ", ".join(
+ [call.get("function", {}).get("name", "未知工具") for call in valid_tool_calls]
+ )
+ logger.info(
+ f"[{self.subheartflow_id}] 模型请求调用{len(valid_tool_calls)}个工具: {tool_calls_str}"
+ )
+
+ # 收集工具执行结果
+ await self._execute_tool_calls(valid_tool_calls, tool_instance)
+ elif not success:
+ logger.warning(f"[{self.subheartflow_id}] {error_msg}")
+ except Exception as e:
+ # 处理总体异常
+ logger.error(f"[{self.subheartflow_id}] 执行LLM请求或处理响应时出错: {e}")
+ logger.error(traceback.format_exc())
+ content = "思考过程中出现错误"
+
+ # 记录最终思考结果
+ name = chat_manager.get_stream_name(self.subheartflow_id)
+ logger.debug(f"[{name}] \nPrompt:\n{prompt}\n\n心流思考结果:\n{content}\n")
+
+ # 处理空响应情况
+ if not content:
+ content = "(不知道该想些什么...)"
+ logger.warning(f"[{self.subheartflow_id}] LLM返回空结果,思考失败。")
+
+ # ---------- 6. 更新思考状态并返回结果 ----------
+ # 更新当前思考内容
+ self.update_current_mind(content)
+
+ return self.current_mind, self.past_mind
+
+ async def _execute_tool_calls(self, tool_calls, tool_instance):
+ """
+ 执行一组工具调用并收集结果
+
+ 参数:
+ tool_calls: 工具调用列表
+ tool_instance: 工具使用器实例
+ """
+ tool_results = []
+ structured_info = {} # 动态生成键
+
+ # 执行所有工具调用
+ for tool_call in tool_calls:
+ try:
+ result = await tool_instance._execute_tool_call(tool_call)
+ if result:
+ tool_results.append(result)
+
+ # 使用工具名称作为键
+ tool_name = result["name"]
+ if tool_name not in structured_info:
+ structured_info[tool_name] = []
+
+ structured_info[tool_name].append({"name": result["name"], "content": result["content"]})
+ except Exception as tool_e:
+ logger.error(f"[{self.subheartflow_id}] 工具执行失败: {tool_e}")
+
+ # 如果有工具结果,记录并更新结构化信息
+ if structured_info:
+ logger.debug(f"工具调用收集到结构化信息: {safe_json_dumps(structured_info, ensure_ascii=False)}")
+ self.structured_info = structured_info
+
+ def update_current_mind(self, response):
+ self.past_mind.append(self.current_mind)
+ self.current_mind = response
+
+
+init_prompt()
diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py
index d5f7ed86..cd32136a 100644
--- a/src/heart_flow/subheartflow_manager.py
+++ b/src/heart_flow/subheartflow_manager.py
@@ -11,7 +11,7 @@ from src.plugins.chat.chat_stream import chat_manager
# 导入心流相关类
from src.heart_flow.sub_heartflow import SubHeartflow, ChatState
-from src.heart_flow.mai_state_manager import MaiState, MaiStateInfo
+from src.heart_flow.mai_state_manager import MaiStateInfo
from .observation import ChattingObservation
# 初始化日志记录器
@@ -23,41 +23,27 @@ subheartflow_manager_log_config = LogConfig(
logger = get_module_logger("subheartflow_manager", config=subheartflow_manager_log_config)
# 子心流管理相关常量
-INACTIVE_THRESHOLD_SECONDS = 1200 # 子心流不活跃超时时间(秒)
+INACTIVE_THRESHOLD_SECONDS = 3600 # 子心流不活跃超时时间(秒)
class SubHeartflowManager:
"""管理所有活跃的 SubHeartflow 实例。"""
- def __init__(self):
+ def __init__(self, mai_state_info: MaiStateInfo):
self.subheartflows: Dict[Any, "SubHeartflow"] = {}
self._lock = asyncio.Lock() # 用于保护 self.subheartflows 的访问
+ self.mai_state_info: MaiStateInfo = mai_state_info # 存储传入的 MaiStateInfo 实例
def get_all_subheartflows(self) -> List["SubHeartflow"]:
"""获取所有当前管理的 SubHeartflow 实例列表 (快照)。"""
return list(self.subheartflows.values())
- def get_all_subheartflows_ids(self) -> List[Any]:
- """获取所有当前管理的 SubHeartflow ID 列表。"""
- return list(self.subheartflows.keys())
-
- def get_subheartflow(self, subheartflow_id: Any) -> Optional["SubHeartflow"]:
- """获取指定 ID 的 SubHeartflow 实例。"""
- # 注意:这里没有加锁,假设读取操作相对安全或在已知上下文中调用
- # 如果并发写操作很多,get 也应该加锁
- subflow = self.subheartflows.get(subheartflow_id)
- if subflow:
- subflow.last_active_time = time.time() # 获取时更新活动时间
- return subflow
-
- async def create_or_get_subheartflow(
- self, subheartflow_id: Any, mai_states: MaiStateInfo
- ) -> Optional["SubHeartflow"]:
+ async def get_or_create_subheartflow(self, subheartflow_id: Any) -> Optional["SubHeartflow"]:
"""获取或创建指定ID的子心流实例
Args:
subheartflow_id: 子心流唯一标识符
- mai_states: 当前麦麦状态信息
+ # mai_states 参数已被移除,使用 self.mai_state_info
Returns:
成功返回SubHeartflow实例,失败返回None
@@ -74,11 +60,12 @@ class SubHeartflowManager:
# logger.debug(f"获取到已存在的子心流: {subheartflow_id}")
return subflow
- # 创建新的子心流实例
- logger.info(f"子心流 {subheartflow_id} 不存在,正在创建...")
try:
- # 初始化子心流
- new_subflow = SubHeartflow(subheartflow_id, mai_states)
+ # 初始化子心流, 传入存储的 mai_state_info
+ new_subflow = SubHeartflow(subheartflow_id, self.mai_state_info)
+
+ # 异步初始化
+ await new_subflow.initialize()
# 添加聊天观察者
observation = ChattingObservation(chat_id=subheartflow_id)
@@ -86,7 +73,8 @@ class SubHeartflowManager:
# 注册子心流
self.subheartflows[subheartflow_id] = new_subflow
- logger.info(f"子心流 {subheartflow_id} 创建成功")
+ heartflow_name = chat_manager.get_stream_name(subheartflow_id) or subheartflow_id
+ logger.info(f"[{heartflow_name}] 开始看消息")
# 启动后台任务
asyncio.create_task(new_subflow.subheartflow_start_working())
@@ -96,7 +84,7 @@ class SubHeartflowManager:
logger.error(f"创建子心流 {subheartflow_id} 失败: {e}", exc_info=True)
return None
- async def stop_subheartflow(self, subheartflow_id: Any, reason: str) -> bool:
+ async def sleep_subheartflow(self, subheartflow_id: Any, reason: str) -> bool:
"""停止指定的子心流并清理资源"""
subheartflow = self.subheartflows.get(subheartflow_id)
if not subheartflow:
@@ -109,12 +97,7 @@ class SubHeartflowManager:
# 设置状态为ABSENT释放资源
if subheartflow.chat_state.chat_status != ChatState.ABSENT:
logger.debug(f"[子心流管理] 设置 {stream_name} 状态为ABSENT")
- states_num = (
- self.count_subflows_by_state(ChatState.ABSENT),
- self.count_subflows_by_state(ChatState.CHAT),
- self.count_subflows_by_state(ChatState.FOCUSED),
- )
- await subheartflow.set_chat_state(ChatState.ABSENT, states_num)
+ await subheartflow.change_chat_state(ChatState.ABSENT)
else:
logger.debug(f"[子心流管理] {stream_name} 已是ABSENT状态")
except Exception as e:
@@ -138,27 +121,26 @@ class SubHeartflowManager:
logger.warning(f"[子心流管理] {stream_name} 已被提前移除")
return False
- def cleanup_inactive_subheartflows(self, max_age_seconds=INACTIVE_THRESHOLD_SECONDS):
- """识别并返回需要清理的不活跃子心流(id, 原因)"""
+ def get_inactive_subheartflows(self, max_age_seconds=INACTIVE_THRESHOLD_SECONDS):
+ """识别并返回需要清理的不活跃(处于ABSENT状态超过一小时)子心流(id, 原因)"""
current_time = time.time()
flows_to_stop = []
for subheartflow_id, subheartflow in list(self.subheartflows.items()):
- # 只检查有interest_chatting的子心流
- if hasattr(subheartflow, "interest_chatting") and subheartflow.interest_chatting:
- last_interact = subheartflow.interest_chatting.last_interaction_time
- if max_age_seconds and (current_time - last_interact) > max_age_seconds:
- reason = f"不活跃时间({current_time - last_interact:.0f}s) > 阈值({max_age_seconds}s)"
- name = chat_manager.get_stream_name(subheartflow_id) or subheartflow_id
- logger.debug(f"[清理] 标记 {name} 待移除: {reason}")
- flows_to_stop.append((subheartflow_id, reason))
+ state = subheartflow.chat_state.chat_status
+ if state != ChatState.ABSENT:
+ continue
+ subheartflow.update_last_chat_state_time()
+ absent_last_time = subheartflow.chat_state_last_time
+ if max_age_seconds and (current_time - absent_last_time) > max_age_seconds:
+ flows_to_stop.append(subheartflow_id)
- if flows_to_stop:
- logger.info(f"[清理] 发现 {len(flows_to_stop)} 个不活跃子心流")
return flows_to_stop
- async def enforce_subheartflow_limits(self, current_mai_state: MaiState):
+ async def enforce_subheartflow_limits(self):
"""根据主状态限制停止超额子心流(优先停不活跃的)"""
+ # 使用 self.mai_state_info 获取当前状态和限制
+ current_mai_state = self.mai_state_info.get_current_state()
normal_limit = current_mai_state.get_normal_chat_max_num()
focused_limit = current_mai_state.get_focused_chat_max_num()
logger.debug(f"[限制] 状态:{current_mai_state.value}, 普通限:{normal_limit}, 专注限:{focused_limit}")
@@ -181,7 +163,7 @@ class SubHeartflowManager:
logger.info(f"[限制] 普通聊天超额({len(normal_flows)}>{normal_limit}), 停止{excess}个")
normal_flows.sort(key=lambda x: x[1])
for flow_id, _ in normal_flows[:excess]:
- if await self.stop_subheartflow(flow_id, f"普通聊天超额(限{normal_limit})"):
+ if await self.sleep_subheartflow(flow_id, f"普通聊天超额(限{normal_limit})"):
stopped += 1
# 处理专注聊天超额(需重新统计)
@@ -195,7 +177,7 @@ class SubHeartflowManager:
logger.info(f"[限制] 专注聊天超额({len(focused_flows)}>{focused_limit}), 停止{excess}个")
focused_flows.sort(key=lambda x: x[1])
for flow_id, _ in focused_flows[:excess]:
- if await self.stop_subheartflow(flow_id, f"专注聊天超额(限{focused_limit})"):
+ if await self.sleep_subheartflow(flow_id, f"专注聊天超额(限{focused_limit})"):
stopped += 1
if stopped:
@@ -203,8 +185,10 @@ class SubHeartflowManager:
else:
logger.debug(f"[限制] 无需停止, 当前总数:{len(self.subheartflows)}")
- async def activate_random_subflows_to_chat(self, current_mai_state: MaiState):
+ async def activate_random_subflows_to_chat(self):
"""主状态激活时,随机选择ABSENT子心流进入CHAT状态"""
+ # 使用 self.mai_state_info 获取当前状态和限制
+ current_mai_state = self.mai_state_info.get_current_state()
limit = current_mai_state.get_normal_chat_max_num()
if limit <= 0:
logger.info("[激活] 当前状态不允许CHAT子心流")
@@ -231,13 +215,15 @@ class SubHeartflowManager:
logger.debug(f"[激活] 正在激活子心流{stream_name}")
- states_num = (
- self.count_subflows_by_state(ChatState.ABSENT),
- self.count_subflows_by_state(ChatState.CHAT),
- self.count_subflows_by_state(ChatState.FOCUSED),
- )
+ # --- 限额检查 --- #
+ current_chat_count = self.count_subflows_by_state(ChatState.CHAT)
+ if current_chat_count >= limit:
+ logger.warning(f"[激活] 跳过{stream_name}, 普通聊天已达上限 ({current_chat_count}/{limit})")
+ continue # 跳过此子心流,继续尝试激活下一个
+ # --- 结束限额检查 --- #
- await flow.set_chat_state(ChatState.CHAT, states_num)
+ # 移除 states_num 参数
+ await flow.change_chat_state(ChatState.CHAT)
if flow.chat_state.chat_status == ChatState.CHAT:
activated_count += 1
@@ -247,118 +233,108 @@ class SubHeartflowManager:
logger.info(f"[激活] 完成, 成功激活{activated_count}个子心流")
async def deactivate_all_subflows(self):
- """停用所有子心流(主状态变为OFFLINE时调用)"""
- logger.info("[停用] 开始停用所有子心流")
- flow_ids = list(self.subheartflows.keys())
+ """将所有子心流的状态更改为 ABSENT (例如主状态变为OFFLINE时调用)"""
+ # logger.info("[停用] 开始将所有子心流状态设置为 ABSENT")
+ # 使用 list() 创建一个当前值的快照,防止在迭代时修改字典
+ flows_to_update = list(self.subheartflows.values())
- if not flow_ids:
- logger.info("[停用] 无活跃子心流")
+ if not flows_to_update:
+ logger.debug("[停用] 无活跃子心流,无需操作")
return
- stopped_count = 0
- for flow_id in flow_ids:
- if await self.stop_subheartflow(flow_id, "主状态离线"):
- stopped_count += 1
+ changed_count = 0
+ for subflow in flows_to_update:
+ flow_id = subflow.subheartflow_id
+ stream_name = chat_manager.get_stream_name(flow_id) or flow_id
+ # 再次检查子心流是否仍然存在于管理器中,以防万一在迭代过程中被移除
- logger.info(f"[停用] 完成, 尝试停止{len(flow_ids)}个, 成功{stopped_count}个")
+ if subflow.chat_state.chat_status != ChatState.ABSENT:
+ logger.debug(
+ f"正在将子心流 {stream_name} 的状态从 {subflow.chat_state.chat_status.value} 更改为 ABSENT"
+ )
+ try:
+ # 调用 change_chat_state 将状态设置为 ABSENT
+ await subflow.change_chat_state(ChatState.ABSENT)
+ # 验证状态是否真的改变了
+ if (
+ flow_id in self.subheartflows
+ and self.subheartflows[flow_id].chat_state.chat_status == ChatState.ABSENT
+ ):
+ changed_count += 1
+ else:
+ logger.warning(
+ f"[停用] 尝试更改子心流 {stream_name} 状态后,状态仍未变为 ABSENT 或子心流已消失。"
+ )
+ except Exception as e:
+ logger.error(f"[停用] 更改子心流 {stream_name} 状态为 ABSENT 时出错: {e}", exc_info=True)
+ else:
+ logger.debug(f"[停用] 子心流 {stream_name} 已处于 ABSENT 状态,无需更改。")
- async def evaluate_interest_and_promote(self, current_mai_state: MaiStateInfo):
+ logger.info(
+ f"下限完成,共处理 {len(flows_to_update)} 个子心流,成功将 {changed_count} 个子心流的状态更改为 ABSENT。"
+ )
+
+ async def evaluate_interest_and_promote(self):
"""评估子心流兴趣度,满足条件且未达上限则提升到FOCUSED状态(基于start_hfc_probability)"""
- log_prefix_manager = "[子心流管理器-兴趣评估]"
- logger.debug(f"{log_prefix_manager} 开始周期... 当前状态: {current_mai_state.get_current_state().value}")
+ log_prefix = "[兴趣评估]"
+ # 使用 self.mai_state_info 获取当前状态和限制
+ current_state = self.mai_state_info.get_current_state()
+ focused_limit = current_state.get_focused_chat_max_num()
+
+ if int(time.time()) % 20 == 0: # 每20秒输出一次
+ logger.debug(f"{log_prefix} 当前状态 ({current_state.value}) 可以在{focused_limit}个群激情聊天")
- # 获取 FOCUSED 状态的数量上限
- current_state_enum = current_mai_state.get_current_state()
- focused_limit = current_state_enum.get_focused_chat_max_num()
if focused_limit <= 0:
- logger.debug(
- f"{log_prefix_manager} 当前状态 ({current_state_enum.value}) 不允许 FOCUSED 子心流, 跳过提升检查。"
- )
+ # logger.debug(f"{log_prefix} 当前状态 ({current_state.value}) 不允许 FOCUSED 子心流")
return
- # 获取当前 FOCUSED 状态的数量 (初始值)
current_focused_count = self.count_subflows_by_state(ChatState.FOCUSED)
- logger.debug(f"{log_prefix_manager} 专注上限: {focused_limit}, 当前专注数: {current_focused_count}")
+ if current_focused_count >= focused_limit:
+ logger.debug(f"{log_prefix} 已达专注上限 ({current_focused_count}/{focused_limit})")
+ return
- # 使用快照安全遍历
- subflows_snapshot = list(self.subheartflows.values())
- promoted_count = 0 # 记录本次提升的数量
- try:
- for sub_hf in subflows_snapshot:
- flow_id = sub_hf.subheartflow_id
- stream_name = chat_manager.get_stream_name(flow_id) or flow_id
- log_prefix_flow = f"[{stream_name}]"
+ for sub_hf in list(self.subheartflows.values()):
+ flow_id = sub_hf.subheartflow_id
+ stream_name = chat_manager.get_stream_name(flow_id) or flow_id
- # 只处理 CHAT 状态的子心流
+ # 跳过非CHAT状态或已经是FOCUSED状态的子心流
+ if sub_hf.chat_state.chat_status == ChatState.FOCUSED:
+ continue
+
+ from .mai_state_manager import enable_unlimited_hfc_chat
+
+ if not enable_unlimited_hfc_chat:
if sub_hf.chat_state.chat_status != ChatState.CHAT:
continue
- # 检查是否满足提升概率
- should_hfc = random.random() < sub_hf.interest_chatting.start_hfc_probability
- if not should_hfc:
- continue
+ # 检查是否满足提升概率
+ if random.random() >= sub_hf.interest_chatting.start_hfc_probability:
+ continue
- # --- 关键检查:检查 FOCUSED 数量是否已达上限 ---
- # 注意:在循环内部再次获取当前数量,因为之前的提升可能已经改变了计数
- # 使用已经记录并在循环中更新的 current_focused_count
- if current_focused_count >= focused_limit:
- logger.debug(
- f"{log_prefix_manager} {log_prefix_flow} 达到专注上限 ({current_focused_count}/{focused_limit}), 无法提升。概率={sub_hf.interest_chatting.start_hfc_probability:.2f}"
- )
- continue # 跳过这个子心流,继续检查下一个
+ # 再次检查是否达到上限
+ if current_focused_count >= focused_limit:
+ logger.debug(f"{log_prefix} [{stream_name}] 已达专注上限")
+ break
- # --- 执行提升 ---
- # 获取当前实例以检查最新状态 (防御性编程)
- current_subflow = self.subheartflows.get(flow_id)
- if not current_subflow or current_subflow.chat_state.chat_status != ChatState.CHAT:
- logger.warning(f"{log_prefix_manager} {log_prefix_flow} 尝试提升时状态已改变或实例消失,跳过。")
- continue
+ # 获取最新状态并执行提升
+ current_subflow = self.subheartflows.get(flow_id)
+ if not current_subflow:
+ continue
- logger.info(
- f"{log_prefix_manager} {log_prefix_flow} 兴趣评估触发升级 (prob={sub_hf.interest_chatting.start_hfc_probability:.2f}, 上限:{focused_limit}, 当前:{current_focused_count}) -> FOCUSED"
- )
+ logger.info(
+ f"{log_prefix} [{stream_name}] 触发 认真水群 (概率={current_subflow.interest_chatting.start_hfc_probability:.2f})"
+ )
- states_num = (
- self.count_subflows_by_state(ChatState.ABSENT),
- self.count_subflows_by_state(ChatState.CHAT), # 这个值在提升前计算
- current_focused_count, # 这个值在提升前计算
- )
+ # 执行状态提升
+ await current_subflow.change_chat_state(ChatState.FOCUSED)
- # --- 状态设置 ---
- original_state = current_subflow.chat_state.chat_status # 记录原始状态
- await current_subflow.set_chat_state(ChatState.FOCUSED, states_num)
+ # 验证提升结果
+ if (
+ final_subflow := self.subheartflows.get(flow_id)
+ ) and final_subflow.chat_state.chat_status == ChatState.FOCUSED:
+ current_focused_count += 1
- # --- 状态验证 ---
- final_subflow = self.subheartflows.get(flow_id)
- if final_subflow:
- final_state = final_subflow.chat_state.chat_status
- if final_state == ChatState.FOCUSED:
- logger.debug(
- f"{log_prefix_manager} {log_prefix_flow} 成功从 {original_state.value} 升级到 FOCUSED 状态"
- )
- promoted_count += 1
- # 提升成功后,更新当前专注计数,以便后续检查能使用最新值
- current_focused_count += 1
- elif final_state == original_state: # 状态未变
- logger.warning(
- f"{log_prefix_manager} {log_prefix_flow} 尝试从 {original_state.value} 升级 FOCUSED 失败,状态仍为: {final_state.value} (可能被内部逻辑阻止)"
- )
- else: # 状态变成其他了?
- logger.warning(
- f"{log_prefix_manager} {log_prefix_flow} 尝试从 {original_state.value} 升级 FOCUSED 后状态变为 {final_state.value}"
- )
- else: # 子心流消失了?
- logger.warning(f"{log_prefix_manager} {log_prefix_flow} 升级后验证时子心流 {flow_id} 消失")
-
- except Exception as e:
- logger.error(f"{log_prefix_manager} 兴趣评估周期出错: {e}", exc_info=True)
-
- if promoted_count > 0:
- logger.info(f"{log_prefix_manager} 评估周期结束, 成功提升 {promoted_count} 个子心流到 FOCUSED。")
- else:
- logger.debug(f"{log_prefix_manager} 评估周期结束, 未提升任何子心流。")
-
- async def randomly_deactivate_subflows(self, deactivation_probability: float = 0.3):
+ async def randomly_deactivate_subflows(self, deactivation_probability: float = 0.1):
"""以一定概率将 FOCUSED 或 CHAT 状态的子心流回退到 ABSENT 状态。"""
log_prefix_manager = "[子心流管理器-随机停用]"
logger.debug(f"{log_prefix_manager} 开始随机停用检查... (概率: {deactivation_probability:.0%})")
@@ -367,13 +343,6 @@ class SubHeartflowManager:
subflows_snapshot = list(self.subheartflows.values())
deactivated_count = 0
- # 预先计算状态数量,因为 set_chat_state 需要
- states_num_before = (
- self.count_subflows_by_state(ChatState.ABSENT),
- self.count_subflows_by_state(ChatState.CHAT),
- self.count_subflows_by_state(ChatState.FOCUSED),
- )
-
try:
for sub_hf in subflows_snapshot:
flow_id = sub_hf.subheartflow_id
@@ -399,7 +368,7 @@ class SubHeartflowManager:
# --- 状态设置 --- #
# 注意:这里传递的状态数量是 *停用前* 的状态数量
- await current_subflow.set_chat_state(ChatState.ABSENT, states_num_before)
+ await current_subflow.change_chat_state(ChatState.ABSENT)
# --- 状态验证 (可选) ---
final_subflow = self.subheartflows.get(flow_id)
@@ -410,7 +379,6 @@ class SubHeartflowManager:
f"{log_prefix_manager} {log_prefix_flow} 成功从 {current_state.value} 停用到 ABSENT 状态"
)
deactivated_count += 1
- # 注意:停用后不需要更新 states_num_before,因为它只用于 set_chat_state 的限制检查
else:
logger.warning(
f"{log_prefix_manager} {log_prefix_flow} 尝试停用到 ABSENT 后状态仍为 {final_state.value}"
@@ -453,7 +421,7 @@ class SubHeartflowManager:
for subheartflow in self.subheartflows.values():
# 检查子心流是否活跃(非ABSENT状态)
if subheartflow.chat_state.chat_status != ChatState.ABSENT:
- minds.append(subheartflow.current_mind)
+ minds.append(subheartflow.sub_mind.current_mind)
return minds
def update_main_mind_in_subflows(self, main_mind: str):
@@ -465,44 +433,17 @@ class SubHeartflowManager:
)
logger.debug(f"[子心流管理器] 更新了{updated_count}个子心流的主想法")
- async def deactivate_subflow(self, subheartflow_id: Any):
- """停用并移除指定的子心流。"""
+ async def delete_subflow(self, subheartflow_id: Any):
+ """删除指定的子心流。"""
async with self._lock:
subflow = self.subheartflows.pop(subheartflow_id, None)
if subflow:
- logger.info(f"正在停用 SubHeartflow: {subheartflow_id}...")
+ logger.info(f"正在删除 SubHeartflow: {subheartflow_id}...")
try:
- # --- 调用 shutdown 方法 ---
+ # 调用 shutdown 方法确保资源释放
await subflow.shutdown()
- # --- 结束调用 ---
- logger.info(f"SubHeartflow {subheartflow_id} 已成功停用。")
+ logger.info(f"SubHeartflow {subheartflow_id} 已成功删除。")
except Exception as e:
- logger.error(f"停用 SubHeartflow {subheartflow_id} 时出错: {e}", exc_info=True)
+ logger.error(f"删除 SubHeartflow {subheartflow_id} 时出错: {e}", exc_info=True)
else:
- logger.warning(f"尝试停用不存在的 SubHeartflow: {subheartflow_id}")
-
- async def cleanup_inactive_subflows(self, inactive_threshold_seconds: int):
- """清理长时间不活跃的子心流。"""
- current_time = time.time()
- inactive_ids = []
- # 不加锁地迭代,识别不活跃的 ID
- for sub_id, subflow in self.subheartflows.items():
- # 检查 last_active_time 是否存在且是数值
- last_active = getattr(subflow, "last_active_time", 0)
- if isinstance(last_active, (int, float)):
- if current_time - last_active > inactive_threshold_seconds:
- inactive_ids.append(sub_id)
- logger.info(
- f"发现不活跃的 SubHeartflow: {sub_id} (上次活跃: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(last_active))})"
- )
- else:
- logger.warning(f"SubHeartflow {sub_id} 的 last_active_time 无效: {last_active}。跳过清理检查。")
-
- if inactive_ids:
- logger.info(f"准备清理 {len(inactive_ids)} 个不活跃的 SubHeartflows: {inactive_ids}")
- # 逐个停用(deactivate_subflow 会加锁)
- tasks = [self.deactivate_subflow(sub_id) for sub_id in inactive_ids]
- await asyncio.gather(*tasks)
- logger.info("不活跃的 SubHeartflows 清理完成。")
- # else:
- # logger.debug("没有发现不活跃的 SubHeartflows 需要清理。")
+ logger.warning(f"尝试删除不存在的 SubHeartflow: {subheartflow_id}")
diff --git a/src/main.py b/src/main.py
index 62fa70a6..a2d8fc51 100644
--- a/src/main.py
+++ b/src/main.py
@@ -3,7 +3,7 @@ import time
from .plugins.utils.statistic import LLMStatistics
from .plugins.moods.moods import MoodManager
from .plugins.schedule.schedule_generator import bot_schedule
-from .plugins.chat.emoji_manager import emoji_manager
+from .plugins.emoji_system.emoji_manager import emoji_manager
from .plugins.person_info.person_info import person_info_manager
from .plugins.willing.willing_manager import willing_manager
from .plugins.chat.chat_stream import chat_manager
@@ -128,7 +128,6 @@ class MainSystem:
self.print_mood_task(),
self.remove_recalled_message_task(),
emoji_manager.start_periodic_check_register(),
- # emoji_manager.start_periodic_register(),
self.app.run(),
self.server.run(),
]
@@ -155,7 +154,7 @@ class MainSystem:
"""打印情绪状态"""
while True:
self.mood_manager.print_mood_status()
- await asyncio.sleep(30)
+ await asyncio.sleep(60)
@staticmethod
async def remove_recalled_message_task():
diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py
index 5b399f06..4e39483b 100644
--- a/src/plugins/PFC/action_planner.py
+++ b/src/plugins/PFC/action_planner.py
@@ -1,5 +1,6 @@
+import time
from typing import Tuple
-from src.common.logger import get_module_logger
+from src.common.logger import get_module_logger, LogConfig, PFC_ACTION_PLANNER_STYLE_CONFIG
from ..models.utils_model import LLMRequest
from ...config.config import global_config
from .chat_observer import ChatObserver
@@ -8,9 +9,16 @@ from src.individuality.individuality import Individuality
from .observation_info import ObservationInfo
from .conversation_info import ConversationInfo
-logger = get_module_logger("action_planner")
+pfc_action_log_config = LogConfig(
+ console_format=PFC_ACTION_PLANNER_STYLE_CONFIG["console_format"],
+ file_format=PFC_ACTION_PLANNER_STYLE_CONFIG["file_format"],
+)
+
+logger = get_module_logger("action_planner", config=pfc_action_log_config)
+# 注意:这个 ActionPlannerInfo 类似乎没有在 ActionPlanner 中使用,
+# 如果确实没用,可以考虑移除,但暂时保留以防万一。
class ActionPlannerInfo:
def __init__(self):
self.done_action = []
@@ -19,17 +27,19 @@ class ActionPlannerInfo:
self.memory_list = []
+# ActionPlanner 类定义,顶格
class ActionPlanner:
"""行动规划器"""
def __init__(self, stream_id: str):
self.llm = LLMRequest(
- model=global_config.llm_normal,
- temperature=global_config.llm_normal["temp"],
- max_tokens=1000,
+ model=global_config.llm_PFC_action_planner,
+ temperature=global_config.llm_PFC_action_planner["temp"],
+ max_tokens=1500,
request_type="action_planning",
)
- self.personality_info = Individuality.get_instance().get_prompt(type="personality", x_person=2, level=2)
+ self.personality_info = Individuality.get_instance().get_prompt(type="personality", x_person=2, level=3)
+ self.identity_detail_info = Individuality.get_instance().get_prompt(type="identity", x_person=2, level=2)
self.name = global_config.BOT_NICKNAME
self.chat_observer = ChatObserver.get_instance(stream_id)
@@ -43,140 +53,255 @@ class ActionPlanner:
Returns:
Tuple[str, str]: (行动类型, 行动原因)
"""
+ # --- 获取 Bot 上次发言时间信息 ---
+ time_since_last_bot_message_info = ""
+ try:
+ bot_id = str(global_config.BOT_QQ)
+ if hasattr(observation_info, "chat_history") and observation_info.chat_history:
+ for i in range(len(observation_info.chat_history) - 1, -1, -1):
+ msg = observation_info.chat_history[i]
+ if not isinstance(msg, dict):
+ continue
+ sender_info = msg.get("user_info", {})
+ sender_id = str(sender_info.get("user_id")) if isinstance(sender_info, dict) else None
+ msg_time = msg.get("time")
+ if sender_id == bot_id and msg_time:
+ time_diff = time.time() - msg_time
+ if time_diff < 60.0:
+ time_since_last_bot_message_info = (
+ f"提示:你上一条成功发送的消息是在 {time_diff:.1f} 秒前。\n"
+ )
+ break
+ else:
+ logger.debug("Observation info chat history is empty or not available for bot time check.")
+ except AttributeError:
+ logger.warning("ObservationInfo object might not have chat_history attribute yet for bot time check.")
+ except Exception as e:
+ logger.warning(f"获取 Bot 上次发言时间时出错: {e}")
+ # --- 获取 Bot 上次发言时间信息结束 ---
+
+ timeout_context = ""
+ try: # 添加 try-except 以增加健壮性
+ if hasattr(conversation_info, "goal_list") and conversation_info.goal_list:
+ last_goal_tuple = conversation_info.goal_list[-1]
+ if isinstance(last_goal_tuple, tuple) and len(last_goal_tuple) > 0:
+ last_goal_text = last_goal_tuple[0]
+ if isinstance(last_goal_text, str) and "分钟,思考接下来要做什么" in last_goal_text:
+ try:
+ timeout_minutes_text = last_goal_text.split(",")[0].replace("你等待了", "")
+ timeout_context = f"重要提示:你刚刚因为对方长时间({timeout_minutes_text})没有回复而结束了等待,这可能代表在对方看来本次聊天已结束,请基于此情况规划下一步,不要重复等待前的发言。\n"
+ except Exception:
+ timeout_context = "重要提示:你刚刚因为对方长时间没有回复而结束了等待,这可能代表在对方看来本次聊天已结束,请基于此情况规划下一步,不要重复等待前的发言。\n"
+ else:
+ logger.debug("Conversation info goal_list is empty or not available for timeout check.")
+ except AttributeError:
+ logger.warning("ConversationInfo object might not have goal_list attribute yet for timeout check.")
+ except Exception as e:
+ logger.warning(f"检查超时目标时出错: {e}")
+
# 构建提示词
- logger.debug(f"开始规划行动:当前目标: {conversation_info.goal_list}")
+ logger.debug(f"开始规划行动:当前目标: {getattr(conversation_info, 'goal_list', '不可用')}") # 使用 getattr
- # 构建对话目标
+ # 构建对话目标 (goals_str)
goals_str = ""
- if conversation_info.goal_list:
- for goal_reason in conversation_info.goal_list:
- # 处理字典或元组格式
- if isinstance(goal_reason, tuple):
- # 假设元组的第一个元素是目标,第二个元素是原因
- goal = goal_reason[0]
- reasoning = goal_reason[1] if len(goal_reason) > 1 else "没有明确原因"
- elif isinstance(goal_reason, dict):
- goal = goal_reason.get("goal")
- reasoning = goal_reason.get("reasoning", "没有明确原因")
- else:
- # 如果是其他类型,尝试转为字符串
- goal = str(goal_reason)
- reasoning = "没有明确原因"
+ try: # 添加 try-except
+ if hasattr(conversation_info, "goal_list") and conversation_info.goal_list:
+ for goal_reason in conversation_info.goal_list:
+ if isinstance(goal_reason, tuple) and len(goal_reason) > 0:
+ goal = goal_reason[0]
+ reasoning = goal_reason[1] if len(goal_reason) > 1 else "没有明确原因"
+ elif isinstance(goal_reason, dict):
+ goal = goal_reason.get("goal", "目标内容缺失")
+ reasoning = goal_reason.get("reasoning", "没有明确原因")
+ else:
+ goal = str(goal_reason)
+ reasoning = "没有明确原因"
+ goal = str(goal) if goal is not None else "目标内容缺失"
+ reasoning = str(reasoning) if reasoning is not None else "没有明确原因"
+ goals_str += f"- 目标:{goal}\n 原因:{reasoning}\n"
+ if not goals_str: # 如果循环后 goals_str 仍为空
+ goals_str = "- 目前没有明确对话目标,请考虑设定一个。\n"
+ except AttributeError:
+ logger.warning("ConversationInfo object might not have goal_list attribute yet.")
+ goals_str = "- 获取对话目标时出错。\n"
+ except Exception as e:
+ logger.error(f"构建对话目标字符串时出错: {e}")
+ goals_str = "- 构建对话目标时出错。\n"
- goal_str = f"目标:{goal},产生该对话目标的原因:{reasoning}\n"
- goals_str += goal_str
- else:
- goal = "目前没有明确对话目标"
- reasoning = "目前没有明确对话目标,最好思考一个对话目标"
- goals_str = f"目标:{goal},产生该对话目标的原因:{reasoning}\n"
-
- # 获取聊天历史记录
- chat_history_list = (
- observation_info.chat_history[-20:]
- if len(observation_info.chat_history) >= 20
- else observation_info.chat_history
- )
+ # 获取聊天历史记录 (chat_history_text)
chat_history_text = ""
- for msg in chat_history_list:
- chat_history_text += f"{msg.get('detailed_plain_text', '')}\n"
+ try:
+ if hasattr(observation_info, "chat_history") and observation_info.chat_history:
+ chat_history_list = observation_info.chat_history[-20:]
+ for msg in chat_history_list:
+ if isinstance(msg, dict) and "detailed_plain_text" in msg:
+ chat_history_text += f"{msg.get('detailed_plain_text', '')}\n"
+ elif isinstance(msg, str):
+ chat_history_text += f"{msg}\n"
+ if not chat_history_text: # 如果历史记录是空列表
+ chat_history_text = "还没有聊天记录。\n"
+ else:
+ chat_history_text = "还没有聊天记录。\n"
- if observation_info.new_messages_count > 0:
- new_messages_list = observation_info.unprocessed_messages
-
- chat_history_text += f"有{observation_info.new_messages_count}条新消息:\n"
- for msg in new_messages_list:
- chat_history_text += f"{msg.get('detailed_plain_text', '')}\n"
-
- observation_info.clear_unprocessed_messages()
-
- personality_text = f"你的名字是{self.name},{self.personality_info}"
-
- # 构建action历史文本
- action_history_list = (
- conversation_info.done_action[-10:]
- if len(conversation_info.done_action) >= 10
- else conversation_info.done_action
- )
- action_history_text = "你之前做的事情是:"
- for action in action_history_list:
- if isinstance(action, dict):
- action_type = action.get("action")
- action_reason = action.get("reason")
- action_status = action.get("status")
- if action_status == "recall":
- action_history_text += (
- f"原本打算:{action_type},但是因为有新消息,你发现这个行动不合适,所以你没做\n"
+ if hasattr(observation_info, "new_messages_count") and observation_info.new_messages_count > 0:
+ if hasattr(observation_info, "unprocessed_messages") and observation_info.unprocessed_messages:
+ new_messages_list = observation_info.unprocessed_messages
+ chat_history_text += f"--- 以下是 {observation_info.new_messages_count} 条新消息 ---\n"
+ for msg in new_messages_list:
+ if isinstance(msg, dict) and "detailed_plain_text" in msg:
+ chat_history_text += f"{msg.get('detailed_plain_text', '')}\n"
+ elif isinstance(msg, str):
+ chat_history_text += f"{msg}\n"
+ # 清理消息应该由调用者或 observation_info 内部逻辑处理,这里不再调用 clear
+ # if hasattr(observation_info, 'clear_unprocessed_messages'):
+ # observation_info.clear_unprocessed_messages()
+ else:
+ logger.warning(
+ "ObservationInfo has new_messages_count > 0 but unprocessed_messages is empty or missing."
)
- elif action_status == "done":
- action_history_text += f"你之前做了:{action_type},原因:{action_reason}\n"
- elif isinstance(action, tuple):
- # 假设元组的格式是(action_type, action_reason, action_status)
- action_type = action[0] if len(action) > 0 else "未知行动"
- action_reason = action[1] if len(action) > 1 else "未知原因"
- action_status = action[2] if len(action) > 2 else "done"
- if action_status == "recall":
- action_history_text += (
- f"原本打算:{action_type},但是因为有新消息,你发现这个行动不合适,所以你没做\n"
- )
- elif action_status == "done":
- action_history_text += f"你之前做了:{action_type},原因:{action_reason}\n"
+ except AttributeError:
+ logger.warning("ObservationInfo object might be missing expected attributes for chat history.")
+ chat_history_text = "获取聊天记录时出错。\n"
+ except Exception as e:
+ logger.error(f"处理聊天记录时发生未知错误: {e}")
+ chat_history_text = "处理聊天记录时出错。\n"
- prompt = f"""{personality_text}。现在你在参与一场QQ聊天,请分析以下内容,根据信息决定下一步行动:
+ # 构建 Persona 文本 (persona_text)
+ identity_details_only = self.identity_detail_info
+ identity_addon = ""
+ if isinstance(identity_details_only, str):
+ pronouns = ["你", "我", "他"]
+ # original_details = identity_details_only
+ for p in pronouns:
+ if identity_details_only.startswith(p):
+ identity_details_only = identity_details_only[len(p) :]
+ break
+ if identity_details_only.endswith("。"):
+ identity_details_only = identity_details_only[:-1]
+ cleaned_details = identity_details_only.strip(",, ")
+ if cleaned_details:
+ identity_addon = f"并且{cleaned_details}"
+ persona_text = f"你的名字是{self.name},{self.personality_info}{identity_addon}。"
-当前对话目标:{goals_str}
+ # --- 构建更清晰的行动历史和上一次行动结果 ---
+ action_history_summary = "你最近执行的行动历史:\n"
+ last_action_context = "关于你【上一次尝试】的行动:\n"
-{action_history_text}
+ action_history_list = []
+ try: # 添加 try-except
+ if hasattr(conversation_info, "done_action") and conversation_info.done_action:
+ action_history_list = conversation_info.done_action[-5:]
+ else:
+ logger.debug("Conversation info done_action is empty or not available.")
+ except AttributeError:
+ logger.warning("ConversationInfo object might not have done_action attribute yet.")
+ except Exception as e:
+ logger.error(f"访问行动历史时出错: {e}")
-最近的对话记录:
-{chat_history_text}
+ if not action_history_list:
+ action_history_summary += "- 还没有执行过行动。\n"
+ last_action_context += "- 这是你规划的第一个行动。\n"
+ else:
+ for i, action_data in enumerate(action_history_list):
+ action_type = "未知"
+ plan_reason = "未知"
+ status = "未知"
+ final_reason = ""
+ action_time = ""
-请你接下去想想要你要做什么,可以发言,可以等待,可以倾听,可以调取知识。注意不同行动类型的要求,不要重复发言:
-行动类型:
-fetch_knowledge: 需要调取知识,当需要专业知识或特定信息时选择
-wait: 当你做出了发言,对方尚未回复时暂时等待对方的回复
-listening: 倾听对方发言,当你认为对方发言尚未结束时采用
-direct_reply: 不符合上述情况,回复对方,注意不要过多或者重复发言
-rethink_goal: 重新思考对话目标,当发现对话目标不合适时选择,会重新思考对话目标
-end_conversation: 结束对话,长时间没回复或者当你觉得谈话暂时结束时选择,停止该场对话
+ if isinstance(action_data, dict):
+ action_type = action_data.get("action", "未知")
+ plan_reason = action_data.get("plan_reason", "未知规划原因")
+ status = action_data.get("status", "未知")
+ final_reason = action_data.get("final_reason", "")
+ action_time = action_data.get("time", "")
+ elif isinstance(action_data, tuple):
+ if len(action_data) > 0:
+ action_type = action_data[0]
+ if len(action_data) > 1:
+ plan_reason = action_data[1]
+ if len(action_data) > 2:
+ status = action_data[2]
+ if status == "recall" and len(action_data) > 3:
+ final_reason = action_data[3]
-请以JSON格式输出,包含以下字段:
-1. action: 行动类型,注意你之前的行为
-2. reason: 选择该行动的原因,注意你之前的行为(简要解释)
+ reason_text = f", 失败/取消原因: {final_reason}" if final_reason else ""
+ summary_line = f"- 时间:{action_time}, 尝试行动:'{action_type}', 状态:{status}{reason_text}"
+ action_history_summary += summary_line + "\n"
+
+ if i == len(action_history_list) - 1:
+ last_action_context += f"- 上次【规划】的行动是: '{action_type}'\n"
+ last_action_context += f"- 当时规划的【原因】是: {plan_reason}\n"
+ if status == "done":
+ last_action_context += "- 该行动已【成功执行】。\n"
+ elif status == "recall":
+ last_action_context += "- 但该行动最终【未能执行/被取消】。\n"
+ if final_reason:
+ last_action_context += f"- 【重要】失败/取消的具体原因是: “{final_reason}”\n"
+ else:
+ last_action_context += "- 【重要】失败/取消原因未明确记录。\n"
+ else:
+ last_action_context += f"- 该行动当前状态: {status}\n"
+
+ # --- 构建最终的 Prompt ---
+ prompt = f"""{persona_text}。现在你在参与一场QQ私聊,请根据以下【所有信息】审慎且灵活的决策下一步行动,可以发言,可以等待,可以倾听,可以调取知识:
+
+【当前对话目标】
+{goals_str if goals_str.strip() else "- 目前没有明确对话目标,请考虑设定一个。"}
+
+
+【最近行动历史概要】
+{action_history_summary}
+【上一次行动的详细情况和结果】
+{last_action_context}
+【时间和超时提示】
+{time_since_last_bot_message_info}{timeout_context}
+【最近的对话记录】(包括你已成功发送的消息 和 新收到的消息)
+{chat_history_text if chat_history_text.strip() else "还没有聊天记录。"}
+
+------
+可选行动类型以及解释:
+etch_knowledge: 需要调取知识,当需要专业知识或特定信息时选择,对方若提到你太认识的人名或实体也可以尝试
+wait: 暂时不说话,等待对方回复(尤其是在你刚发言后、或上次发言因重复、发言过多被拒时、或不确定做什么时,这是较安全的选择)
+listening: 倾听对方发言,当你认为对方话才说到一半,发言明显未结束时采用
+direct_reply: 直接回复或发送新消息,允许适当的追问和深入话题,**但是避免在因重复被拒后立即使用,也不要在对方没有回复的情况下过多的“消息轰炸”或重复发言**
+rethink_goal: 重新思考对话目标,当发现对话目标不再适用或对话卡住时选择,注意私聊的环境是灵活的,有可能需要经常选择
+end_conversation: 结束对话,对方长时间没回复或者当你觉得对话告一段落时可以选择
+
+请以JSON格式输出你的决策:
+{{
+ "action": "选择的行动类型 (必须是上面列表中的一个)",
+ "reason": "选择该行动的详细原因 (必须有解释你是如何根据“上一次行动结果”、“对话记录”和自身设定人设做出合理判断的,如果你连续发言,必须记录已经发言了几次)"
+}}
注意:请严格按照JSON格式输出,不要包含任何其他内容。"""
- logger.debug(f"发送到LLM的提示词: {prompt}")
+ logger.debug(f"发送到LLM的提示词 (已更新): {prompt}")
try:
content, _ = await self.llm.generate_response_async(prompt)
logger.debug(f"LLM原始返回内容: {content}")
- # 使用简化函数提取JSON内容
success, result = get_items_from_json(
- content, "action", "reason", default_values={"action": "direct_reply", "reason": "没有明确原因"}
+ content,
+ "action",
+ "reason",
+ default_values={"action": "wait", "reason": "LLM返回格式错误或未提供原因,默认等待"},
)
- if not success:
- return "direct_reply", "JSON解析失败,选择直接回复"
-
- action = result["action"]
- reason = result["reason"]
+ action = result.get("action", "wait")
+ reason = result.get("reason", "LLM未提供原因,默认等待")
# 验证action类型
- if action not in [
- "direct_reply",
- "fetch_knowledge",
- "wait",
- "listening",
- "rethink_goal",
- "end_conversation",
- ]:
- logger.warning(f"未知的行动类型: {action},默认使用listening")
- action = "listening"
+ valid_actions = ["direct_reply", "fetch_knowledge", "wait", "listening", "rethink_goal", "end_conversation"]
+ if action not in valid_actions:
+ logger.warning(f"LLM返回了未知的行动类型: '{action}',强制改为 wait")
+ reason = f"(原始行动'{action}'无效,已强制改为wait) {reason}"
+ action = "wait"
logger.info(f"规划的行动: {action}")
logger.info(f"行动原因: {reason}")
return action, reason
except Exception as e:
- logger.error(f"规划行动时出错: {str(e)}")
- return "direct_reply", "发生错误,选择直接回复"
+ logger.error(f"规划行动时调用 LLM 或处理结果出错: {str(e)}")
+ return "wait", f"行动规划处理中发生错误,暂时等待: {str(e)}"
diff --git a/src/plugins/PFC/chat_observer.py b/src/plugins/PFC/chat_observer.py
index 1239af7a..697833c8 100644
--- a/src/plugins/PFC/chat_observer.py
+++ b/src/plugins/PFC/chat_observer.py
@@ -3,7 +3,7 @@ import asyncio
import traceback
from typing import Optional, Dict, Any, List
from src.common.logger import get_module_logger
-from ..message.message_base import UserInfo
+from maim_message import UserInfo
from ...config.config import global_config
from .chat_states import NotificationManager, create_new_message_notification, create_cold_chat_notification
from .message_storage import MongoDBMessageStorage
@@ -119,6 +119,7 @@ class ChatObserver:
self.last_cold_chat_check = current_time
# 判断是否冷场
+ is_cold = False
if self.last_message_time is None:
is_cold = True
else:
@@ -354,7 +355,7 @@ class ChatObserver:
Returns:
List[Dict[str, Any]]: 缓存的消息历史列表
"""
- return self.message_cache[:limit]
+ return self.message_cache[-limit:]
def get_last_message(self) -> Optional[Dict[str, Any]]:
"""获取最后一条消息
@@ -364,7 +365,7 @@ class ChatObserver:
"""
if not self.message_cache:
return None
- return self.message_cache[0]
+ return self.message_cache[-1]
def __str__(self):
return f"ChatObserver for {self.stream_id}"
diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py
index 9502b755..dc1e6a34 100644
--- a/src/plugins/PFC/conversation.py
+++ b/src/plugins/PFC/conversation.py
@@ -1,5 +1,10 @@
+import time
import asyncio
import datetime
+
+# from .message_storage import MongoDBMessageStorage
+from src.plugins.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat
+from ...config.config import global_config
from typing import Dict, Any
from ..chat.message import Message
from .pfc_types import ConversationState
@@ -10,7 +15,7 @@ from .observation_info import ObservationInfo
from .conversation_info import ConversationInfo
from .reply_generator import ReplyGenerator
from ..chat.chat_stream import ChatStream
-from ..message.message_base import UserInfo
+from maim_message import UserInfo
from src.plugins.chat.chat_stream import chat_manager
from .pfc_KnowledgeFetcher import KnowledgeFetcher
from .waiter import Waiter
@@ -70,7 +75,41 @@ class Conversation:
logger.error(f"初始化对话实例:注册信息组件失败: {e}")
logger.error(traceback.format_exc())
raise
+ try:
+ logger.info(f"为 {self.stream_id} 加载初始聊天记录...")
+ initial_messages = await get_raw_msg_before_timestamp_with_chat( #
+ chat_id=self.stream_id,
+ timestamp=time.time(),
+ limit=30, # 加载最近30条作为初始上下文,可以调整
+ )
+ if initial_messages:
+ # 将加载的消息填充到 ObservationInfo 的 chat_history
+ self.observation_info.chat_history = initial_messages
+ self.observation_info.chat_history_count = len(initial_messages)
+ # 更新 ObservationInfo 中的时间戳等信息
+ last_msg = initial_messages[-1]
+ self.observation_info.last_message_time = last_msg.get("time")
+ last_user_info = UserInfo.from_dict(last_msg.get("user_info", {}))
+ self.observation_info.last_message_sender = last_user_info.user_id
+ self.observation_info.last_message_content = last_msg.get("processed_plain_text", "")
+
+ # (可选)可以遍历 initial_messages 来设置 last_bot_speak_time 和 last_user_speak_time
+ # 这里为了简化,只用了最后一条消息的时间,如果需要精确的发言者时间需要遍历
+
+ logger.info(
+ f"成功加载 {len(initial_messages)} 条初始聊天记录。最后一条消息时间: {self.observation_info.last_message_time}"
+ )
+
+ # 让 ChatObserver 从加载的最后一条消息之后开始同步
+ self.chat_observer.last_message_time = self.observation_info.last_message_time
+ self.chat_observer.last_message_read = last_msg # 更新 observer 的最后读取记录
+ else:
+ logger.info("没有找到初始聊天记录。")
+
+ except Exception as load_err:
+ logger.error(f"加载初始聊天记录时出错: {load_err}")
+ # 出错也要继续,只是没有历史记录而已
# 组件准备完成,启动该论对话
self.should_continue = True
asyncio.create_task(self.start())
@@ -86,24 +125,79 @@ class Conversation:
async def _plan_and_action_loop(self):
"""思考步,PFC核心循环模块"""
- # 获取最近的消息历史
while self.should_continue:
- # 使用决策信息来辅助行动规划
- action, reason = await self.action_planner.plan(self.observation_info, self.conversation_info)
- if self._check_new_messages_after_planning():
- continue
+ try:
+ # --- 在规划前记录当前新消息数量 ---
+ initial_new_message_count = 0
+ if hasattr(self.observation_info, "new_messages_count"):
+ initial_new_message_count = self.observation_info.new_messages_count
+ else:
+ logger.warning("ObservationInfo missing 'new_messages_count' before planning.")
- # 执行行动
- await self._handle_action(action, reason, self.observation_info, self.conversation_info)
+ # 使用决策信息来辅助行动规划
+ action, reason = await self.action_planner.plan(
+ self.observation_info, self.conversation_info
+ ) # 注意:plan 函数内部现在不应再调用 clear_unprocessed_messages
- for goal in self.conversation_info.goal_list:
- # 检查goal是否为元组类型,如果是元组则使用索引访问,如果是字典则使用get方法
- if isinstance(goal, tuple):
- # 假设元组的第一个元素是目标内容
- print(f"goal: {goal}")
- if goal[0] == "结束对话":
- self.should_continue = False
- break
+ # --- 规划后检查是否有 *更多* 新消息到达 ---
+ current_new_message_count = 0
+ if hasattr(self.observation_info, "new_messages_count"):
+ current_new_message_count = self.observation_info.new_messages_count
+ else:
+ logger.warning("ObservationInfo missing 'new_messages_count' after planning.")
+
+ if current_new_message_count > initial_new_message_count:
+ # 只有当规划期间消息数量 *增加* 了,才认为需要重新规划
+ logger.info(
+ f"规划期间发现新增消息 ({initial_new_message_count} -> {current_new_message_count}),跳过本次行动,重新规划"
+ )
+ await asyncio.sleep(0.1) # 短暂延时
+ continue # 跳过本次行动,重新规划
+
+ # --- 如果没有在规划期间收到更多新消息,则准备执行行动 ---
+
+ # --- 清理未处理消息:移到这里,在执行动作前 ---
+ # 只有当确实有新消息被 planner 看到,并且 action 是要处理它们的时候才清理
+ if initial_new_message_count > 0 and action == "direct_reply":
+ if hasattr(self.observation_info, "clear_unprocessed_messages"):
+ # 确保 clear_unprocessed_messages 方法存在
+ logger.debug(f"准备执行 direct_reply,清理 {initial_new_message_count} 条规划时已知的新消息。")
+ self.observation_info.clear_unprocessed_messages()
+ # 手动重置计数器,确保状态一致性(理想情况下 clear 方法会做这个)
+ if hasattr(self.observation_info, "new_messages_count"):
+ self.observation_info.new_messages_count = 0
+ else:
+ logger.error("无法清理未处理消息: ObservationInfo 缺少 clear_unprocessed_messages 方法!")
+ # 这里可能需要考虑是否继续执行 action,或者抛出错误
+
+ # --- 执行行动 ---
+ await self._handle_action(action, reason, self.observation_info, self.conversation_info)
+
+ goal_ended = False
+ if hasattr(self.conversation_info, "goal_list") and self.conversation_info.goal_list:
+ for goal in self.conversation_info.goal_list:
+ if isinstance(goal, tuple) and len(goal) > 0 and goal[0] == "结束对话":
+ goal_ended = True
+ break
+ elif isinstance(goal, dict) and goal.get("goal") == "结束对话":
+ goal_ended = True
+ break
+
+ if goal_ended:
+ self.should_continue = False
+ logger.info("检测到'结束对话'目标,停止循环。")
+ # break # 可以选择在这里直接跳出循环
+
+ except Exception as loop_err:
+ logger.error(f"PFC主循环出错: {loop_err}")
+ logger.error(traceback.format_exc())
+ # 发生严重错误时可以考虑停止,或者至少等待一下再继续
+ await asyncio.sleep(1) # 发生错误时等待1秒
+ # 添加短暂的异步睡眠
+ if self.should_continue: # 只有在还需要继续循环时才 sleep
+ await asyncio.sleep(0.1) # 等待 0.1 秒,给其他任务执行时间
+
+ logger.info(f"PFC 循环结束 for stream_id: {self.stream_id}") # 添加日志表明循环正常结束
def _check_new_messages_after_planning(self):
"""检查在规划后是否有新消息"""
@@ -113,8 +207,7 @@ class Conversation:
return True
return False
- @staticmethod
- def _convert_to_message(msg_dict: Dict[str, Any]) -> Message:
+ def _convert_to_message(self, msg_dict: Dict[str, Any]) -> Message:
"""将消息字典转换为Message对象"""
try:
chat_info = msg_dict.get("chat_info", {})
@@ -124,7 +217,7 @@ class Conversation:
return Message(
message_id=msg_dict["message_id"],
chat_stream=chat_stream,
- timestamp=msg_dict["time"],
+ time=msg_dict["time"],
user_info=user_info,
processed_plain_text=msg_dict.get("processed_plain_text", ""),
detailed_plain_text=msg_dict.get("detailed_plain_text", ""),
@@ -137,92 +230,189 @@ class Conversation:
self, action: str, reason: str, observation_info: ObservationInfo, conversation_info: ConversationInfo
):
"""处理规划的行动"""
+
logger.info(f"执行行动: {action}, 原因: {reason}")
- # 记录action历史,先设置为stop,完成后再设置为done
- conversation_info.done_action.append(
- {
- "action": action,
- "reason": reason,
- "status": "start",
- "time": datetime.datetime.now().strftime("%H:%M:%S"),
- }
- )
+ # 记录action历史,先设置为start,完成后再设置为done (这个 update 移到后面执行成功后再做)
+ current_action_record = {
+ "action": action,
+ "plan_reason": reason, # 使用 plan_reason 存储规划原因
+ "status": "start", # 初始状态为 start
+ "time": datetime.datetime.now().strftime("%H:%M:%S"),
+ "final_reason": None,
+ }
+ conversation_info.done_action.append(current_action_record)
+ # 获取刚刚添加记录的索引,方便后面更新状态
+ action_index = len(conversation_info.done_action) - 1
+ # --- 根据不同的 action 执行 ---
if action == "direct_reply":
- self.waiter.wait_accumulated_time = 0
+ max_reply_attempts = 3 # 设置最大尝试次数(与 reply_checker.py 中的 max_retries 保持一致或稍大)
+ reply_attempt_count = 0
+ is_suitable = False
+ need_replan = False
+ check_reason = "未进行尝试"
+ final_reply_to_send = ""
- self.state = ConversationState.GENERATING
- self.generated_reply = await self.reply_generator.generate(observation_info, conversation_info)
- print(f"生成回复: {self.generated_reply}")
+ while reply_attempt_count < max_reply_attempts and not is_suitable:
+ reply_attempt_count += 1
+ logger.info(f"尝试生成回复 (第 {reply_attempt_count}/{max_reply_attempts} 次)...")
+ self.state = ConversationState.GENERATING
- # # 检查回复是否合适
- # is_suitable, reason, need_replan = await self.reply_generator.check_reply(
- # self.generated_reply,
- # self.current_goal
- # )
+ # 1. 生成回复
+ self.generated_reply = await self.reply_generator.generate(observation_info, conversation_info)
+ logger.info(f"第 {reply_attempt_count} 次生成的回复: {self.generated_reply}")
- if self._check_new_messages_after_planning():
- logger.info("333333发现新消息,重新考虑行动")
- conversation_info.done_action[-1].update(
+ # 2. 检查回复
+ self.state = ConversationState.CHECKING
+ try:
+ current_goal_str = conversation_info.goal_list[0][0] if conversation_info.goal_list else ""
+ # 注意:这里传递的是 reply_attempt_count - 1 作为 retry_count 给 checker
+ is_suitable, check_reason, need_replan = await self.reply_generator.check_reply(
+ reply=self.generated_reply,
+ goal=current_goal_str,
+ chat_history=observation_info.chat_history,
+ retry_count=reply_attempt_count - 1, # 传递当前尝试次数(从0开始计数)
+ )
+ logger.info(
+ f"第 {reply_attempt_count} 次检查结果: 合适={is_suitable}, 原因='{check_reason}', 需重新规划={need_replan}"
+ )
+
+ if is_suitable:
+ final_reply_to_send = self.generated_reply # 保存合适的回复
+ break # 回复合适,跳出循环
+
+ elif need_replan:
+ logger.warning(f"第 {reply_attempt_count} 次检查建议重新规划,停止尝试。原因: {check_reason}")
+ break # 如果检查器建议重新规划,也停止尝试
+
+ # 如果不合适但不需要重新规划,循环会继续进行下一次尝试
+ except Exception as check_err:
+ logger.error(f"第 {reply_attempt_count} 次调用 ReplyChecker 时出错: {check_err}")
+ check_reason = f"第 {reply_attempt_count} 次检查过程出错: {check_err}"
+ # 如果检查本身出错,可以选择跳出循环或继续尝试
+ # 这里选择跳出循环,避免无限循环在检查错误上
+ break
+
+ # 循环结束,处理最终结果
+ if is_suitable:
+ # 回复合适且已保存在 final_reply_to_send 中
+ # 检查是否有新消息进来 (在所有尝试结束后再检查一次)
+ if self._check_new_messages_after_planning():
+ logger.info("生成回复期间收到新消息,取消发送,重新规划行动")
+ conversation_info.done_action[action_index].update(
+ {
+ "status": "recall",
+ "final_reason": f"有新消息,取消发送: {final_reply_to_send}",
+ "time": datetime.datetime.now().strftime("%H:%M:%S"),
+ }
+ )
+ # 这里直接返回,不执行后续发送和wait
+ return
+
+ # 发送合适的回复
+ self.generated_reply = final_reply_to_send # 确保 self.generated_reply 是最终要发送的内容
+ await self._send_reply()
+
+ # 更新 action 历史状态为 done
+ conversation_info.done_action[action_index].update(
{
- "status": "recall",
+ "status": "done",
"time": datetime.datetime.now().strftime("%H:%M:%S"),
}
)
- return None
- await self._send_reply()
+ else:
+ # 循环结束但没有找到合适的回复(达到最大次数或检查出错/建议重规划)
+ logger.warning(f"经过 {reply_attempt_count} 次尝试,未能生成合适的回复。最终原因: {check_reason}")
+ conversation_info.done_action[action_index].update(
+ {
+ "status": "recall", # 标记为 recall 因为没有成功发送
+ "final_reason": f"尝试{reply_attempt_count}次后失败: {check_reason}",
+ "time": datetime.datetime.now().strftime("%H:%M:%S"),
+ }
+ )
- conversation_info.done_action[-1].update(
+ # 执行 Wait 操作
+ logger.info("由于无法生成合适回复,执行 'wait' 操作...")
+ self.state = ConversationState.WAITING
+ # 直接调用 wait 方法
+ await self.waiter.wait(self.conversation_info)
+ # 可以选择添加一条新的 action 记录来表示这个 wait
+ wait_action_record = {
+ "action": "wait",
+ "plan_reason": "因 direct_reply 多次尝试失败而执行的后备等待",
+ "status": "done", # wait 完成后可以认为是 done
+ "time": datetime.datetime.now().strftime("%H:%M:%S"),
+ "final_reason": None,
+ }
+ conversation_info.done_action.append(wait_action_record)
+
+ elif action == "fetch_knowledge":
+ self.waiter.wait_accumulated_time = 0
+ self.state = ConversationState.FETCHING
+ knowledge = "TODO:知识"
+ topic = "TODO:关键词"
+ logger.info(f"假装获取到知识{knowledge},关键词是: {topic}")
+ if knowledge:
+ pass # 简单处理
+ # 标记 action 为 done
+ conversation_info.done_action[action_index].update(
{
"status": "done",
"time": datetime.datetime.now().strftime("%H:%M:%S"),
}
)
- return None
-
- elif action == "fetch_knowledge":
- self.waiter.wait_accumulated_time = 0
-
- self.state = ConversationState.FETCHING
- knowledge = "TODO:知识"
- topic = "TODO:关键词"
-
- logger.info(f"假装获取到知识{knowledge},关键词是: {topic}")
-
- if knowledge:
- if topic not in self.conversation_info.knowledge_list:
- self.conversation_info.knowledge_list.append({"topic": topic, "knowledge": knowledge})
- return None
- else:
- self.conversation_info.knowledge_list[topic] += knowledge
- return None
- return None
elif action == "rethink_goal":
self.waiter.wait_accumulated_time = 0
-
self.state = ConversationState.RETHINKING
await self.goal_analyzer.analyze_goal(conversation_info, observation_info)
- return None
+ # 标记 action 为 done
+ conversation_info.done_action[action_index].update(
+ {
+ "status": "done",
+ "time": datetime.datetime.now().strftime("%H:%M:%S"),
+ }
+ )
elif action == "listening":
self.state = ConversationState.LISTENING
logger.info("倾听对方发言...")
await self.waiter.wait_listening(conversation_info)
- return None
+ # listening 和 wait 通常在完成后不需要标记为 done,因为它们是持续状态,
+ # 但如果需要记录,可以在 waiter 返回后标记。目前逻辑是 waiter 返回后主循环继续。
+ # 为了统一,可以暂时在这里也标记一下(或者都不标记)
+ conversation_info.done_action[action_index].update(
+ {
+ "status": "done", # 或 "completed"
+ "time": datetime.datetime.now().strftime("%H:%M:%S"),
+ }
+ )
elif action == "end_conversation":
- self.should_continue = False
+ self.should_continue = False # 设置循环停止标志
logger.info("决定结束对话...")
- return None
+ # 标记 action 为 done
+ conversation_info.done_action[action_index].update(
+ {
+ "status": "done",
+ "time": datetime.datetime.now().strftime("%H:%M:%S"),
+ }
+ )
+ # 这里不需要 return,主循环会在下一轮检查 should_continue
- else: # wait
+ else: # 对应 'wait' 动作
self.state = ConversationState.WAITING
logger.info("等待更多信息...")
await self.waiter.wait(self.conversation_info)
- return None
+ # 同 listening,可以考虑是否标记状态
+ conversation_info.done_action[action_index].update(
+ {
+ "status": "done", # 或 "completed"
+ "time": datetime.datetime.now().strftime("%H:%M:%S"),
+ }
+ )
async def _send_timeout_message(self):
"""发送超时结束消息"""
@@ -245,12 +435,52 @@ class Conversation:
return
try:
- await self.direct_sender.send_message(chat_stream=self.chat_stream, content=self.generated_reply)
- self.chat_observer.trigger_update() # 触发立即更新
- if not await self.chat_observer.wait_for_update():
- logger.warning("等待消息更新超时")
+ # 外层 try: 捕获发送消息和后续处理中的主要错误
+ current_time = time.time() # 获取当前时间戳
+ reply_content = self.generated_reply # 获取要发送的内容
+
+ # 发送消息
+ await self.direct_sender.send_message(chat_stream=self.chat_stream, content=reply_content)
+ logger.info(f"消息已发送: {reply_content}") # 可以在发送后加个日志确认
+
+ # --- 添加的立即更新状态逻辑开始 ---
+ try:
+ # 内层 try: 专门捕获手动更新状态时可能出现的错误
+ # 创建一个代表刚刚发送的消息的字典
+ bot_message_info = {
+ "message_id": f"bot_sent_{current_time}", # 创建一个简单的唯一ID
+ "time": current_time,
+ "user_info": UserInfo( # 使用 UserInfo 类构建用户信息
+ user_id=str(global_config.BOT_QQ),
+ user_nickname=global_config.BOT_NICKNAME,
+ platform=self.chat_stream.platform, # 从 chat_stream 获取平台信息
+ ).to_dict(), # 转换为字典格式存储
+ "processed_plain_text": reply_content, # 使用发送的内容
+ "detailed_plain_text": f"{int(current_time)},{global_config.BOT_NICKNAME}:{reply_content}", # 构造一个简单的详细文本, 时间戳取整
+ # 可以根据需要添加其他字段,保持与 observation_info.chat_history 中其他消息结构一致
+ }
+
+ # 直接更新 ObservationInfo 实例
+ if self.observation_info:
+ self.observation_info.chat_history.append(bot_message_info) # 将消息添加到历史记录末尾
+ self.observation_info.last_bot_speak_time = current_time # 更新 Bot 最后发言时间
+ self.observation_info.last_message_time = current_time # 更新最后消息时间
+ logger.debug("已手动将Bot发送的消息添加到 ObservationInfo")
+ else:
+ logger.warning("无法手动更新 ObservationInfo:实例不存在")
+
+ except Exception as update_err:
+ logger.error(f"手动更新 ObservationInfo 时出错: {update_err}")
+ # --- 添加的立即更新状态逻辑结束 ---
+
+ # 原有的触发更新和等待代码
+ self.chat_observer.trigger_update()
+ if not await self.chat_observer.wait_for_update():
+ logger.warning("等待 ChatObserver 更新完成超时")
+
+ self.state = ConversationState.ANALYZING # 更新对话状态
- self.state = ConversationState.ANALYZING
except Exception as e:
- logger.error(f"发送消息失败: {str(e)}")
- self.state = ConversationState.ANALYZING
+ # 这是外层 try 对应的 except
+ logger.error(f"发送消息或更新状态时失败: {str(e)}")
+ self.state = ConversationState.ANALYZING # 出错也要尝试恢复状态
diff --git a/src/plugins/PFC/message_sender.py b/src/plugins/PFC/message_sender.py
index 5a5818ae..8a0f4176 100644
--- a/src/plugins/PFC/message_sender.py
+++ b/src/plugins/PFC/message_sender.py
@@ -2,9 +2,9 @@ from typing import Optional
from src.common.logger import get_module_logger
from ..chat.chat_stream import ChatStream
from ..chat.message import Message
-from ..message.message_base import Seg
+from maim_message import Seg
from src.plugins.chat.message import MessageSending, MessageSet
-from src.plugins.chat.messagesender import message_manager
+from src.plugins.chat.message_sender import message_manager
logger = get_module_logger("message_sender")
@@ -15,8 +15,8 @@ class DirectMessageSender:
def __init__(self):
pass
- @staticmethod
async def send_message(
+ self,
chat_stream: ChatStream,
content: str,
reply_to_message: Optional[Message] = None,
diff --git a/src/plugins/PFC/message_storage.py b/src/plugins/PFC/message_storage.py
index cd6a01e3..55bccb14 100644
--- a/src/plugins/PFC/message_storage.py
+++ b/src/plugins/PFC/message_storage.py
@@ -50,16 +50,21 @@ class MessageStorage(ABC):
class MongoDBMessageStorage(MessageStorage):
"""MongoDB消息存储实现"""
+ def __init__(self):
+ self.db = db
+
async def get_messages_after(self, chat_id: str, message_time: float) -> List[Dict[str, Any]]:
- query = {"chat_id": chat_id, "time": {"$gt": message_time}}
+ query = {"chat_id": chat_id}
# print(f"storage_check_message: {message_time}")
- return list(db.messages.find(query).sort("time", 1))
+ query["time"] = {"$gt": message_time}
+
+ return list(self.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]]:
query = {"chat_id": chat_id, "time": {"$lt": time_point}}
- messages = list(db.messages.find(query).sort("time", -1).limit(limit))
+ messages = list(self.db.messages.find(query).sort("time", -1).limit(limit))
# 将消息按时间正序排列
messages.reverse()
@@ -68,7 +73,7 @@ class MongoDBMessageStorage(MessageStorage):
async def has_new_messages(self, chat_id: str, after_time: float) -> bool:
query = {"chat_id": chat_id, "time": {"$gt": after_time}}
- return db.messages.find_one(query) is not None
+ return self.db.messages.find_one(query) is not None
# # 创建一个内存消息存储实现,用于测试
diff --git a/src/plugins/PFC/observation_info.py b/src/plugins/PFC/observation_info.py
index f92f1230..4cb6aaaa 100644
--- a/src/plugins/PFC/observation_info.py
+++ b/src/plugins/PFC/observation_info.py
@@ -1,7 +1,7 @@
# Programmable Friendly Conversationalist
# Prefrontal cortex
from typing import List, Optional, Dict, Any, Set
-from ..message.message_base import UserInfo
+from maim_message import UserInfo
import time
from dataclasses import dataclass, field
from src.common.logger import get_module_logger
@@ -120,10 +120,6 @@ class ObservationInfo:
# #spec
# meta_plan_trigger: bool = False
- def __init__(self):
- self.last_message_id = None
- self.chat_observer = None
-
def __post_init__(self):
"""初始化后创建handler"""
self.chat_observer = None
@@ -133,7 +129,7 @@ class ObservationInfo:
"""绑定到指定的chat_observer
Args:
- chat_observer: 要绑定的ChatObserver实例
+ stream_id: 聊天流ID
"""
self.chat_observer = chat_observer
self.chat_observer.notification_manager.register_handler(
@@ -175,8 +171,7 @@ class ObservationInfo:
self.last_bot_speak_time = message["time"]
else:
self.last_user_speak_time = message["time"]
- if user_info.user_id is not None:
- self.active_users.add(str(user_info.user_id))
+ self.active_users.add(user_info.user_id)
self.new_messages_count += 1
self.unprocessed_messages.append(message)
@@ -232,7 +227,7 @@ class ObservationInfo:
"""清空未处理消息列表"""
# 将未处理消息添加到历史记录中
for message in self.unprocessed_messages:
- self.chat_history.append(message) # TODO NEED FIX TYPE???
+ self.chat_history.append(message)
# 清空未处理消息列表
self.has_unread_messages = False
self.unprocessed_messages.clear()
diff --git a/src/plugins/PFC/pfc.py b/src/plugins/PFC/pfc.py
index 1d096cc4..033cf822 100644
--- a/src/plugins/PFC/pfc.py
+++ b/src/plugins/PFC/pfc.py
@@ -6,7 +6,7 @@ import datetime
from typing import List, Optional, Tuple, TYPE_CHECKING
from src.common.logger import get_module_logger
from ..chat.chat_stream import ChatStream
-from ..message.message_base import UserInfo, Seg
+from maim_message import UserInfo, Seg
from ..chat.message import Message
from ..models.utils_model import LLMRequest
from ...config.config import global_config
@@ -34,7 +34,8 @@ class GoalAnalyzer:
model=global_config.llm_normal, temperature=0.7, max_tokens=1000, request_type="conversation_goal"
)
- self.personality_info = Individuality.get_instance().get_prompt(type="personality", x_person=2, level=2)
+ self.personality_info = Individuality.get_instance().get_prompt(type="personality", x_person=2, level=3)
+ self.identity_detail_info = Individuality.get_instance().get_prompt(type="identity", x_person=2, level=2)
self.name = global_config.BOT_NICKNAME
self.nick_name = global_config.BOT_ALIAS_NAMES
self.chat_observer = ChatObserver.get_instance(stream_id)
@@ -93,15 +94,28 @@ class GoalAnalyzer:
observation_info.clear_unprocessed_messages()
- personality_text = f"你的名字是{self.name},{self.personality_info}"
+ identity_details_only = self.identity_detail_info
+ identity_addon = ""
+ if isinstance(identity_details_only, str):
+ pronouns = ["你", "我", "他"]
+ for p in pronouns:
+ if identity_details_only.startswith(p):
+ identity_details_only = identity_details_only[len(p) :]
+ break
+ if identity_details_only.endswith("。"):
+ identity_details_only = identity_details_only[:-1]
+ cleaned_details = identity_details_only.strip(",, ")
+ if cleaned_details:
+ identity_addon = f"并且{cleaned_details}"
+ persona_text = f"你的名字是{self.name},{self.personality_info}{identity_addon}。"
# 构建action历史文本
action_history_list = conversation_info.done_action
action_history_text = "你之前做的事情是:"
for action in action_history_list:
action_history_text += f"{action}\n"
- prompt = f"""{personality_text}。现在你在参与一场QQ聊天,请分析以下聊天记录,并根据你的性格特征确定多个明确的对话目标。
+ prompt = f"""{persona_text}。现在你在参与一场QQ聊天,请分析以下聊天记录,并根据你的性格特征确定多个明确的对话目标。
这些目标应该反映出对话的不同方面和意图。
{action_history_text}
@@ -160,16 +174,16 @@ class GoalAnalyzer:
# 返回第一个目标作为当前主要目标(如果有)
if result:
first_goal = result[0]
- return first_goal.get("goal", ""), "", first_goal.get("reasoning", "")
+ return (first_goal.get("goal", ""), "", first_goal.get("reasoning", ""))
else:
# 单个目标的情况
goal = result.get("goal", "")
reasoning = result.get("reasoning", "")
conversation_info.goal_list.append((goal, reasoning))
- return goal, "", reasoning
+ return (goal, "", reasoning)
# 如果解析失败,返回默认值
- return "", "", ""
+ return ("", "", "")
async def _update_goals(self, new_goal: str, method: str, reasoning: str):
"""更新目标列表
@@ -195,8 +209,7 @@ class GoalAnalyzer:
if len(self.goals) > self.max_goals:
self.goals.pop() # 移除最老的目标
- @staticmethod
- def _calculate_similarity(goal1: str, goal2: str) -> float:
+ def _calculate_similarity(self, goal1: str, goal2: str) -> float:
"""简单计算两个目标之间的相似度
这里使用一个简单的实现,实际可以使用更复杂的文本相似度算法
@@ -244,9 +257,25 @@ class GoalAnalyzer:
sender = "你说"
chat_history_text += f"{time_str},{sender}:{msg.get('processed_plain_text', '')}\n"
- personality_text = f"你的名字是{self.name},{self.personality_info}"
+ identity_details_only = self.identity_detail_info
+ identity_addon = ""
+ if isinstance(identity_details_only, str):
+ pronouns = ["你", "我", "他"]
+ for p in pronouns:
+ if identity_details_only.startswith(p):
+ identity_details_only = identity_details_only[len(p) :]
+ break
+ if identity_details_only.endswith("。"):
+ identity_details_only = identity_details_only[:-1]
+ cleaned_details = identity_details_only.strip(",, ")
+ if cleaned_details:
+ identity_addon = f"并且{cleaned_details}"
- prompt = f"""{personality_text}。现在你在参与一场QQ聊天,
+ persona_text = f"你的名字是{self.name},{self.personality_info}{identity_addon}。"
+ # ===> Persona 文本构建结束 <===
+
+ # --- 修改 Prompt 字符串,使用 persona_text ---
+ prompt = f"""{persona_text}。现在你在参与一场QQ聊天,
当前对话目标:{goal}
产生该对话目标的原因:{reasoning}
@@ -300,8 +329,7 @@ class DirectMessageSender:
self.logger = get_module_logger("direct_sender")
self.storage = MessageStorage()
- @staticmethod
- async def send_via_ws(message: MessageSending) -> None:
+ async def send_via_ws(self, message: MessageSending) -> None:
try:
await global_api.send_message(message)
except Exception as e:
@@ -343,22 +371,11 @@ class DirectMessageSender:
# 处理消息
await message.process()
- message_json = message.to_dict()
+ _message_json = message.to_dict()
# 发送消息
try:
- end_point = global_config.api_urls.get(message.message_info.platform, None)
- if end_point:
- # logger.info(f"发送消息到{end_point}")
- # logger.info(message_json)
- try:
- await global_api.send_message_rest(end_point, message_json)
- except Exception as e:
- logger.error(f"REST方式发送失败,出现错误: {str(e)}")
- logger.info("尝试使用ws发送")
- await self.send_via_ws(message)
- else:
- await self.send_via_ws(message)
+ await self.send_via_ws(message)
logger.success(f"PFC消息已发送: {content}")
except Exception as e:
logger.error(f"PFC消息发送失败: {str(e)}")
diff --git a/src/plugins/PFC/pfc_KnowledgeFetcher.py b/src/plugins/PFC/pfc_KnowledgeFetcher.py
index 7ce7ce7a..95e66c8c 100644
--- a/src/plugins/PFC/pfc_KnowledgeFetcher.py
+++ b/src/plugins/PFC/pfc_KnowledgeFetcher.py
@@ -4,6 +4,7 @@ from src.plugins.memory_system.Hippocampus import HippocampusManager
from ..models.utils_model import LLMRequest
from ...config.config import global_config
from ..chat.message import Message
+from ..knowledge.knowledge_lib import qa_manager
logger = get_module_logger("knowledge_fetcher")
@@ -19,8 +20,26 @@ class KnowledgeFetcher:
request_type="knowledge_fetch",
)
- @staticmethod
- async def fetch(query: str, chat_history: List[Message]) -> Tuple[str, str]:
+ def _lpmm_get_knowledge(self, query: str) -> str:
+ """获取相关知识
+
+ Args:
+ query: 查询内容
+
+ Returns:
+ str: 构造好的,带相关度的知识
+ """
+
+ logger.debug("正在从LPMM知识库中获取知识")
+ try:
+ knowledge_info = qa_manager.get_knowledge(query)
+ logger.debug(f"LPMM知识库查询结果: {knowledge_info:150}")
+ return knowledge_info
+ except Exception as e:
+ logger.error(f"LPMM知识库搜索工具执行失败: {str(e)}")
+ return "未找到匹配的知识"
+
+ async def fetch(self, query: str, chat_history: List[Message]) -> Tuple[str, str]:
"""获取相关知识
Args:
@@ -44,13 +63,16 @@ class KnowledgeFetcher:
max_depth=3,
fast_retrieval=False,
)
-
+ knowledge = ""
if related_memory:
- knowledge = ""
sources = []
for memory in related_memory:
knowledge += memory[1] + "\n"
sources.append(f"记忆片段{memory[0]}")
- return knowledge.strip(), ",".join(sources)
+ knowledge = knowledge.strip(), ",".join(sources)
+
+ knowledge += "现在有以下**知识**可供参考:\n "
+ knowledge += self._lpmm_get_knowledge(query)
+ knowledge += "请记住这些**知识**,并根据**知识**回答问题。\n"
return "未找到相关知识", "无记忆匹配"
diff --git a/src/plugins/PFC/reply_checker.py b/src/plugins/PFC/reply_checker.py
index 0efa46fa..1f6f91dd 100644
--- a/src/plugins/PFC/reply_checker.py
+++ b/src/plugins/PFC/reply_checker.py
@@ -1,11 +1,11 @@
import json
import datetime
-from typing import Tuple
+from typing import Tuple, List, Dict, Any
from src.common.logger import get_module_logger
from ..models.utils_model import LLMRequest
from ...config.config import global_config
from .chat_observer import ChatObserver
-from ..message.message_base import UserInfo
+from maim_message import UserInfo
logger = get_module_logger("reply_checker")
@@ -15,13 +15,15 @@ class ReplyChecker:
def __init__(self, stream_id: str):
self.llm = LLMRequest(
- model=global_config.llm_normal, temperature=0.7, max_tokens=1000, request_type="reply_check"
+ model=global_config.llm_PFC_reply_checker, temperature=0.50, max_tokens=1000, request_type="reply_check"
)
self.name = global_config.BOT_NICKNAME
self.chat_observer = ChatObserver.get_instance(stream_id)
- self.max_retries = 2 # 最大重试次数
+ self.max_retries = 3 # 最大重试次数
- async def check(self, reply: str, goal: str, retry_count: int = 0) -> Tuple[bool, str, bool]:
+ async def check(
+ self, reply: str, goal: str, chat_history: List[Dict[str, Any]], retry_count: int = 0
+ ) -> Tuple[bool, str, bool]:
"""检查生成的回复是否合适
Args:
@@ -32,10 +34,55 @@ class ReplyChecker:
Returns:
Tuple[bool, str, bool]: (是否合适, 原因, 是否需要重新规划)
"""
- # 获取最新的消息记录
- messages = self.chat_observer.get_cached_messages(limit=5)
+ # 不再从 observer 获取,直接使用传入的 chat_history
+ # messages = self.chat_observer.get_cached_messages(limit=20)
chat_history_text = ""
- for msg in messages:
+ try:
+ # 筛选出最近由 Bot 自己发送的消息
+ bot_messages = []
+ for msg in reversed(chat_history):
+ user_info = UserInfo.from_dict(msg.get("user_info", {}))
+ if str(user_info.user_id) == str(global_config.BOT_QQ): # 确保比较的是字符串
+ bot_messages.append(msg.get("processed_plain_text", ""))
+ if len(bot_messages) >= 2: # 只和最近的两条比较
+ break
+ # 进行比较
+ if bot_messages:
+ # 可以用简单比较,或者更复杂的相似度库 (如 difflib)
+ # 简单比较:是否完全相同
+ if reply == bot_messages[0]: # 和最近一条完全一样
+ logger.warning(f"ReplyChecker 检测到回复与上一条 Bot 消息完全相同: '{reply}'")
+ return (
+ False,
+ "回复内容与你上一条发言完全相同,请修改,可以选择深入话题或寻找其它话题或等待",
+ False,
+ ) # 不合适,无需重新规划
+ # 2. 相似度检查 (如果精确匹配未通过)
+ import difflib # 导入 difflib 库
+
+ # 计算编辑距离相似度,ratio() 返回 0 到 1 之间的浮点数
+ similarity_ratio = difflib.SequenceMatcher(None, reply, bot_messages[0]).ratio()
+ logger.debug(f"ReplyChecker - 相似度: {similarity_ratio:.2f}")
+
+ # 设置一个相似度阈值
+ similarity_threshold = 0.9
+ if similarity_ratio > similarity_threshold:
+ logger.warning(
+ f"ReplyChecker 检测到回复与上一条 Bot 消息高度相似 (相似度 {similarity_ratio:.2f}): '{reply}'"
+ )
+ return (
+ False,
+ f"拒绝发送:回复内容与你上一条发言高度相似 (相似度 {similarity_ratio:.2f}),请修改,可以选择深入话题或寻找其它话题或等待。",
+ False,
+ )
+
+ except Exception as e:
+ import traceback
+
+ logger.error(f"检查回复时出错: 类型={type(e)}, 值={e}")
+ logger.error(traceback.format_exc()) # 打印详细的回溯信息
+
+ for msg in chat_history[-20:]:
time_str = datetime.datetime.fromtimestamp(msg["time"]).strftime("%H:%M:%S")
user_info = UserInfo.from_dict(msg.get("user_info", {}))
sender = user_info.user_nickname or f"用户{user_info.user_id}"
@@ -43,7 +90,7 @@ class ReplyChecker:
sender = "你说"
chat_history_text += f"{time_str},{sender}:{msg.get('processed_plain_text', '')}\n"
- prompt = f"""请检查以下回复是否合适:
+ prompt = f"""请检查以下回复或消息是否合适:
当前对话目标:{goal}
最新的对话记录:
@@ -52,12 +99,18 @@ class ReplyChecker:
待检查的回复:
{reply}
-请检查以下几点:
+请结合聊天记录检查以下几点:
1. 回复是否依然符合当前对话目标和实现方式
2. 回复是否与最新的对话记录保持一致性
-3. 回复是否重复发言,重复表达
-4. 回复是否包含违法违规内容(政治敏感、暴力等)
-5. 回复是否以你的角度发言,不要把"你"说的话当做对方说的话,这是你自己说的话
+3. 回复是否重复发言,或重复表达同质内容(尤其是只是换一种方式表达了相同的含义)
+4. 回复是否包含违规内容(例如血腥暴力,政治敏感等)
+5. 回复是否以你的角度发言,不要把"你"说的话当做对方说的话,这是你自己说的话(不要自己回复自己的消息)
+6. 回复是否通俗易懂
+7. 回复是否有些多余,例如在对方没有回复的情况下,依然连续多次“消息轰炸”(尤其是已经连续发送3条信息的情况,这很可能不合理,需要着重判断)
+8. 回复是否使用了完全没必要的修辞
+9. 回复是否逻辑通顺
+10. 回复是否太过冗长了(通常私聊的每条消息长度在20字以内,除非特殊情况)
+11. 在连续多次发送消息的情况下,当前回复是否衔接自然,会不会显得奇怪(例如连续两条消息中部分内容重叠)
请以JSON格式输出,包含以下字段:
1. suitable: 是否合适 (true/false)
diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py
index a27abecd..fe9dab6f 100644
--- a/src/plugins/PFC/reply_generator.py
+++ b/src/plugins/PFC/reply_generator.py
@@ -1,4 +1,4 @@
-from typing import Tuple
+from typing import Tuple, List, Dict, Any
from src.common.logger import get_module_logger
from ..models.utils_model import LLMRequest
from ...config.config import global_config
@@ -16,12 +16,13 @@ class ReplyGenerator:
def __init__(self, stream_id: str):
self.llm = LLMRequest(
- model=global_config.llm_normal,
- temperature=global_config.llm_normal["temp"],
+ model=global_config.llm_PFC_chat,
+ temperature=global_config.llm_PFC_chat["temp"],
max_tokens=300,
request_type="reply_generation",
)
- self.personality_info = Individuality.get_instance().get_prompt(type="personality", x_person=2, level=2)
+ self.personality_info = Individuality.get_instance().get_prompt(type="personality", x_person=2, level=3)
+ self.identity_detail_info = Individuality.get_instance().get_prompt(type="identity", x_person=2, level=2)
self.name = global_config.BOT_NICKNAME
self.chat_observer = ChatObserver.get_instance(stream_id)
self.reply_checker = ReplyChecker(stream_id)
@@ -30,8 +31,11 @@ class ReplyGenerator:
"""生成回复
Args:
- observation_info: 观察信息
- conversation_info: 对话信息
+ goal: 对话目标
+ chat_history: 聊天历史
+ knowledge_cache: 知识缓存
+ previous_reply: 上一次生成的回复(如果有)
+ retry_count: 当前重试次数
Returns:
str: 生成的回复
@@ -82,8 +86,20 @@ class ReplyGenerator:
observation_info.clear_unprocessed_messages()
- personality_text = f"你的名字是{self.name},{self.personality_info}"
-
+ identity_details_only = self.identity_detail_info
+ identity_addon = ""
+ if isinstance(identity_details_only, str):
+ pronouns = ["你", "我", "他"]
+ for p in pronouns:
+ if identity_details_only.startswith(p):
+ identity_details_only = identity_details_only[len(p) :]
+ break
+ if identity_details_only.endswith("。"):
+ identity_details_only = identity_details_only[:-1]
+ cleaned_details = identity_details_only.strip(",, ")
+ if cleaned_details:
+ identity_addon = f"并且{cleaned_details}"
+ persona_text = f"你的名字是{self.name},{self.personality_info}{identity_addon}。"
# 构建action历史文本
action_history_list = (
conversation_info.done_action[-10:]
@@ -114,23 +130,25 @@ class ReplyGenerator:
elif action_status == "done":
action_history_text += f"你之前做了:{action_type},原因:{action_reason}\n"
- prompt = f"""{personality_text}。现在你在参与一场QQ聊天,请根据以下信息生成回复:
+ prompt = f"""{persona_text}。现在你在参与一场QQ私聊,请根据以下信息生成一条新消息:
当前对话目标:{goals_str}
最近的聊天记录:
{chat_history_text}
-请根据上述信息,以你的性格特征生成一个自然、得体的回复。回复应该:
-1. 符合对话目标,以"你"的角度发言
-2. 体现你的性格特征
-3. 自然流畅,像正常聊天一样,简短
+请根据上述信息,结合聊天记录,发一条消息(可以是回复,补充,深入话题,或追问等等)。该消息应该:
+1. 符合对话目标,以"你"的角度发言(不要自己与自己对话!)
+2. 符合你的性格特征和身份细节
+3. 通俗易懂,自然流畅,像正常聊天一样,简短(通常20字以内,除非特殊情况)
4. 适当利用相关知识,但不要生硬引用
+5. 自然、得体,结合聊天记录逻辑合理,且没有重复表达同质内容
请注意把握聊天内容,不要回复的太有条理,可以有个性。请分清"你"和对方说的话,不要把"你"说的话当做对方说的话,这是你自己说的话。
-请你回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,尽量不要说你说过的话
+可以回复得自然随意自然一些,就像真人一样,注意把握聊天内容,整体风格可以平和、简短,不要刻意突出自身学科背景,不要说你说过的话,可以简短,多简短都可以,但是避免冗长。
请你注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。
不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。
+**注意:如果聊天记录中最新的消息是你自己发送的,那么你的思路不应该是“回复”,而是应该紧紧衔接你发送的消息,进行话题的深入,补充,或追问等等,避免与最新消息内容重叠;**
请直接输出回复内容,不需要任何额外格式。"""
@@ -151,10 +169,12 @@ class ReplyGenerator:
return content
except Exception as e:
- logger.error(f"生成回复时出错: {str(e)}")
+ logger.error(f"生成回复时出错: {e}")
return "抱歉,我现在有点混乱,让我重新思考一下..."
- async def check_reply(self, reply: str, goal: str, retry_count: int = 0) -> Tuple[bool, str, bool]:
+ async def check_reply(
+ self, reply: str, goal: str, chat_history: List[Dict[str, Any]], retry_count: int = 0
+ ) -> Tuple[bool, str, bool]:
"""检查回复是否合适
Args:
@@ -165,4 +185,4 @@ class ReplyGenerator:
Returns:
Tuple[bool, str, bool]: (是否合适, 原因, 是否需要重新规划)
"""
- return await self.reply_checker.check(reply, goal, retry_count)
+ return await self.reply_checker.check(reply, goal, chat_history, retry_count)
diff --git a/src/plugins/PFC/waiter.py b/src/plugins/PFC/waiter.py
index 4d47d500..eaf8a768 100644
--- a/src/plugins/PFC/waiter.py
+++ b/src/plugins/PFC/waiter.py
@@ -1,85 +1,76 @@
from src.common.logger import get_module_logger
from .chat_observer import ChatObserver
from .conversation_info import ConversationInfo
-from src.individuality.individuality import Individuality
+
+# from src.individuality.individuality import Individuality # 不再需要
from ...config.config import global_config
import time
import asyncio
logger = get_module_logger("waiter")
+# --- 在这里设定你想要的超时时间(秒) ---
+# 例如: 120 秒 = 2 分钟
+DESIRED_TIMEOUT_SECONDS = 300
+
class Waiter:
- """快 速 等 待"""
+ """等待处理类"""
def __init__(self, stream_id: str):
self.chat_observer = ChatObserver.get_instance(stream_id)
- self.personality_info = Individuality.get_instance().get_prompt(type="personality", x_person=2, level=2)
self.name = global_config.BOT_NICKNAME
-
- self.wait_accumulated_time = 0
+ # self.wait_accumulated_time = 0 # 不再需要累加计时
async def wait(self, conversation_info: ConversationInfo) -> bool:
- """等待
-
- Returns:
- bool: 是否超时(True表示超时)
- """
- # 使用当前时间作为等待开始时间
+ """等待用户新消息或超时"""
wait_start_time = time.time()
- self.chat_observer.waiting_start_time = wait_start_time # 设置等待开始时间
+ logger.info(f"进入常规等待状态 (超时: {DESIRED_TIMEOUT_SECONDS} 秒)...")
while True:
# 检查是否有新消息
if self.chat_observer.new_message_after(wait_start_time):
logger.info("等待结束,收到新消息")
- return False
+ return False # 返回 False 表示不是超时
# 检查是否超时
- if time.time() - wait_start_time > 300:
- self.wait_accumulated_time += 300
-
- logger.info("等待超过300秒,结束对话")
+ elapsed_time = time.time() - wait_start_time
+ if elapsed_time > DESIRED_TIMEOUT_SECONDS:
+ logger.info(f"等待超过 {DESIRED_TIMEOUT_SECONDS} 秒...添加思考目标。")
wait_goal = {
- "goal": f"你等待了{self.wait_accumulated_time / 60}分钟,思考接下来要做什么",
+ "goal": f"你等待了{elapsed_time / 60:.1f}分钟,注意可能在对方看来聊天已经结束,思考接下来要做什么",
"reason": "对方很久没有回复你的消息了",
}
conversation_info.goal_list.append(wait_goal)
- print(f"添加目标: {wait_goal}")
+ logger.info(f"添加目标: {wait_goal}")
+ return True # 返回 True 表示超时
- return True
-
- await asyncio.sleep(1)
- logger.info("等待中...")
+ await asyncio.sleep(5) # 每 5 秒检查一次
+ logger.info("等待中...") # 可以考虑把这个频繁日志注释掉,只在超时或收到消息时输出
async def wait_listening(self, conversation_info: ConversationInfo) -> bool:
- """等待倾听
-
- Returns:
- bool: 是否超时(True表示超时)
- """
- # 使用当前时间作为等待开始时间
+ """倾听用户发言或超时"""
wait_start_time = time.time()
- self.chat_observer.waiting_start_time = wait_start_time # 设置等待开始时间
+ logger.info(f"进入倾听等待状态 (超时: {DESIRED_TIMEOUT_SECONDS} 秒)...")
while True:
# 检查是否有新消息
if self.chat_observer.new_message_after(wait_start_time):
- logger.info("等待结束,收到新消息")
- return False
+ logger.info("倾听等待结束,收到新消息")
+ return False # 返回 False 表示不是超时
# 检查是否超时
- if time.time() - wait_start_time > 300:
- self.wait_accumulated_time += 300
- logger.info("等待超过300秒,结束对话")
+ elapsed_time = time.time() - wait_start_time
+ if elapsed_time > DESIRED_TIMEOUT_SECONDS:
+ logger.info(f"倾听等待超过 {DESIRED_TIMEOUT_SECONDS} 秒...添加思考目标。")
wait_goal = {
- "goal": f"你等待了{self.wait_accumulated_time / 60}分钟,思考接下来要做什么",
+ # 保持 goal 文本一致
+ "goal": f"你等待了{elapsed_time / 60:.1f}分钟,注意可能在对方看来聊天已经结束,思考接下来要做什么",
"reason": "对方话说一半消失了,很久没有回复",
}
conversation_info.goal_list.append(wait_goal)
- print(f"添加目标: {wait_goal}")
+ logger.info(f"添加目标: {wait_goal}")
+ return True # 返回 True 表示超时
- return True
-
- await asyncio.sleep(1)
- logger.info("等待中...")
+ await asyncio.sleep(5) # 每 5 秒检查一次
+ logger.info("倾听等待中...") # 同上,可以考虑注释掉
diff --git a/src/plugins/__init__.py b/src/plugins/__init__.py
index 85de966e..2e057e6f 100644
--- a/src/plugins/__init__.py
+++ b/src/plugins/__init__.py
@@ -4,7 +4,7 @@ MaiMBot插件系统
"""
from .chat.chat_stream import chat_manager
-from .chat.emoji_manager import emoji_manager
+from .emoji_system.emoji_manager import emoji_manager
from .person_info.relationship_manager import relationship_manager
from .moods.moods import MoodManager
from .willing.willing_manager import willing_manager
diff --git a/src/plugins/chat/__init__.py b/src/plugins/chat/__init__.py
index 8d9aa1f8..e5b0b942 100644
--- a/src/plugins/chat/__init__.py
+++ b/src/plugins/chat/__init__.py
@@ -1,4 +1,4 @@
-from .emoji_manager import emoji_manager
+from ..emoji_system.emoji_manager import emoji_manager
from ..person_info.relationship_manager import relationship_manager
from .chat_stream import chat_manager
from .message_sender import message_manager
diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py
index fdb2576a..1202fce2 100644
--- a/src/plugins/chat/bot.py
+++ b/src/plugins/chat/bot.py
@@ -82,8 +82,8 @@ class ChatBot:
logger.debug(f"用户{userinfo.user_id}被禁止回复")
return
- if groupinfo.group_id not in global_config.talk_allowed_groups:
- logger.debug(f"群{groupinfo.group_id}被禁止回复")
+ if groupinfo != None and groupinfo.group_id not in global_config.talk_allowed_groups:
+ logger.trace(f"群{groupinfo.group_id}被禁止回复")
return
if message.message_info.template_info and not message.message_info.template_info.template_default:
diff --git a/src/plugins/chat/chat_stream.py b/src/plugins/chat/chat_stream.py
index ebeaa7c0..9416ebad 100644
--- a/src/plugins/chat/chat_stream.py
+++ b/src/plugins/chat/chat_stream.py
@@ -6,11 +6,16 @@ from typing import Dict, Optional
from ...common.database import db
-from ..message.message_base import GroupInfo, UserInfo
+from maim_message import GroupInfo, UserInfo
-from src.common.logger import get_module_logger
+from src.common.logger import get_module_logger, LogConfig, CHAT_STREAM_STYLE_CONFIG
-logger = get_module_logger("chat_stream")
+chat_stream_log_config = LogConfig(
+ console_format=CHAT_STREAM_STYLE_CONFIG["console_format"],
+ file_format=CHAT_STREAM_STYLE_CONFIG["file_format"],
+)
+
+logger = get_module_logger("chat_stream", config=chat_stream_log_config)
class ChatStream:
diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py
deleted file mode 100644
index cbc8e600..00000000
--- a/src/plugins/chat/emoji_manager.py
+++ /dev/null
@@ -1,595 +0,0 @@
-import asyncio
-import base64
-import hashlib
-import os
-import random
-import time
-import traceback
-from typing import Optional, Tuple
-from PIL import Image
-import io
-
-from ...common.database import db
-from ...config.config import global_config
-from ..chat.utils import get_embedding
-from ..chat.utils_image import ImageManager, image_path_to_base64
-from ..models.utils_model import LLMRequest
-from src.common.logger import get_module_logger, LogConfig, EMOJI_STYLE_CONFIG
-
-emoji_log_config = LogConfig(
- console_format=EMOJI_STYLE_CONFIG["console_format"],
- file_format=EMOJI_STYLE_CONFIG["file_format"],
-)
-
-logger = get_module_logger("emoji", config=emoji_log_config)
-
-
-image_manager = ImageManager()
-
-
-class EmojiManager:
- _instance = None
- EMOJI_DIR = os.path.join("data", "emoji") # 表情包存储目录
-
- def __new__(cls):
- if cls._instance is None:
- cls._instance = super().__new__(cls)
- cls._instance._initialized = False
- return cls._instance
-
- def __init__(self):
- self._scan_task = None
- self.vlm = LLMRequest(model=global_config.vlm, temperature=0.3, max_tokens=1000, request_type="emoji")
- self.llm_emotion_judge = LLMRequest(
- model=global_config.llm_emotion_judge, max_tokens=600, temperature=0.8, request_type="emoji"
- ) # 更高的温度,更少的token(后续可以根据情绪来调整温度)
-
- self.emoji_num = 0
- self.emoji_num_max = global_config.max_emoji_num
- self.emoji_num_max_reach_deletion = global_config.max_reach_deletion
-
- logger.info("启动表情包管理器")
-
- def _ensure_emoji_dir(self):
- """确保表情存储目录存在"""
- os.makedirs(self.EMOJI_DIR, exist_ok=True)
-
- def _update_emoji_count(self):
- """更新表情包数量统计
-
- 检查数据库中的表情包数量并更新到 self.emoji_num
- """
- try:
- self._ensure_db()
- self.emoji_num = db.emoji.count_documents({})
- logger.info(f"[统计] 当前表情包数量: {self.emoji_num}")
- except Exception as e:
- logger.error(f"[错误] 更新表情包数量失败: {str(e)}")
-
- def initialize(self):
- """初始化数据库连接和表情目录"""
- if not self._initialized:
- try:
- self._ensure_emoji_collection()
- self._ensure_emoji_dir()
- self._initialized = True
- # 更新表情包数量
- self._update_emoji_count()
- # 启动时执行一次完整性检查
- self.check_emoji_file_integrity()
- except Exception:
- logger.exception("初始化表情管理器失败")
-
- def _ensure_db(self):
- """确保数据库已初始化"""
- if not self._initialized:
- self.initialize()
- if not self._initialized:
- raise RuntimeError("EmojiManager not initialized")
-
- @staticmethod
- def _ensure_emoji_collection():
- """确保emoji集合存在并创建索引
-
- 这个函数用于确保MongoDB数据库中存在emoji集合,并创建必要的索引。
-
- 索引的作用是加快数据库查询速度:
- - embedding字段的2dsphere索引: 用于加速向量相似度搜索,帮助快速找到相似的表情包
- - tags字段的普通索引: 加快按标签搜索表情包的速度
- - filename字段的唯一索引: 确保文件名不重复,同时加快按文件名查找的速度
-
- 没有索引的话,数据库每次查询都需要扫描全部数据,建立索引后可以大大提高查询效率。
- """
- if "emoji" not in db.list_collection_names():
- db.create_collection("emoji")
- db.emoji.create_index([("embedding", "2dsphere")])
- db.emoji.create_index([("filename", 1)], unique=True)
-
- def record_usage(self, emoji_id: str):
- """记录表情使用次数"""
- try:
- self._ensure_db()
- db.emoji.update_one({"_id": emoji_id}, {"$inc": {"usage_count": 1}})
- except Exception as e:
- logger.error(f"记录表情使用失败: {str(e)}")
-
- async def get_emoji_for_text(self, text: str) -> Optional[Tuple[str, str]]:
- """根据文本内容获取相关表情包
- Args:
- text: 输入文本
- Returns:
- Optional[str]: 表情包文件路径,如果没有找到则返回None
-
-
- 可不可以通过 配置文件中的指令 来自定义使用表情包的逻辑?
- 我觉得可行
-
- """
- try:
- self._ensure_db()
-
- # 获取文本的embedding
- text_for_search = await self._get_kimoji_for_text(text)
- if not text_for_search:
- logger.error("无法获取文本的情绪")
- return None
- text_embedding = await get_embedding(text_for_search, request_type="emoji")
- if not text_embedding:
- logger.error("无法获取文本的embedding")
- return None
-
- try:
- # 获取所有表情包
- all_emojis = [
- e
- for e in db.emoji.find({}, {"_id": 1, "path": 1, "embedding": 1, "description": 1, "blacklist": 1})
- if "blacklist" not in e
- ]
-
- if not all_emojis:
- logger.warning("数据库中没有任何表情包")
- return None
-
- # 计算余弦相似度并排序
- def cosine_similarity(v1, v2):
- if not v1 or not v2:
- return 0
- dot_product = sum(a * b for a, b in zip(v1, v2))
- norm_v1 = sum(a * a for a in v1) ** 0.5
- norm_v2 = sum(b * b for b in v2) ** 0.5
- if norm_v1 == 0 or norm_v2 == 0:
- return 0
- return dot_product / (norm_v1 * norm_v2)
-
- # 计算所有表情包与输入文本的相似度
- emoji_similarities = [
- (emoji, cosine_similarity(text_embedding, emoji.get("embedding", []))) for emoji in all_emojis
- ]
-
- # 按相似度降序排序
- emoji_similarities.sort(key=lambda x: x[1], reverse=True)
-
- # 获取前3个最相似的表情包
- top_10_emojis = emoji_similarities[: 10 if len(emoji_similarities) > 10 else len(emoji_similarities)]
-
- if not top_10_emojis:
- logger.warning("未找到匹配的表情包")
- return None
-
- # 从前3个中随机选择一个
- selected_emoji, similarity = random.choice(top_10_emojis)
-
- if selected_emoji and "path" in selected_emoji:
- # 更新使用次数
- db.emoji.update_one({"_id": selected_emoji["_id"]}, {"$inc": {"usage_count": 1}})
-
- logger.info(
- f"[匹配] 找到表情包: {selected_emoji.get('description', '无描述')} (相似度: {similarity:.4f})"
- )
- # 稍微改一下文本描述,不然容易产生幻觉,描述已经包含 表情包 了
- return selected_emoji["path"], "[ %s ]" % selected_emoji.get("description", "无描述")
-
- except Exception as search_error:
- logger.error(f"[错误] 搜索表情包失败: {str(search_error)}")
- return None
-
- return None
-
- except Exception as e:
- logger.error(f"[错误] 获取表情包失败: {str(e)}")
- return None
-
- @staticmethod
- async def _get_emoji_description(image_base64: str) -> str:
- """获取表情包的标签,使用image_manager的描述生成功能"""
-
- try:
- # 使用image_manager获取描述,去掉前后的方括号和"表情包:"前缀
- description = await image_manager.get_emoji_description(image_base64)
- # 去掉[表情包:xxx]的格式,只保留描述内容
- description = description.strip("[]").replace("表情包:", "")
- return description
-
- except Exception as e:
- logger.error(f"[错误] 获取表情包描述失败: {str(e)}")
- return None
-
- async def _check_emoji(self, image_base64: str, image_format: str) -> str:
- try:
- prompt = (
- f'这是一个表情包,请回答这个表情包是否满足"{global_config.EMOJI_CHECK_PROMPT}"的要求,是则回答是,'
- f"否则回答否,不要出现任何其他内容"
- )
-
- content, _ = await self.vlm.generate_response_for_image(prompt, image_base64, image_format)
- logger.debug(f"[检查] 表情包检查结果: {content}")
- return content
-
- except Exception as e:
- logger.error(f"[错误] 表情包检查失败: {str(e)}")
- return None
-
- async def _get_kimoji_for_text(self, text: str):
- try:
- prompt = (
- f"这是{global_config.BOT_NICKNAME}将要发送的消息内容:\n{text}\n若要为其配上表情包,"
- f"请你输出这个表情包应该表达怎样的情感,应该给人什么样的感觉,不要太简洁也不要太长,"
- f'注意不要输出任何对消息内容的分析内容,只输出"一种什么样的感觉"中间的形容词部分。'
- )
-
- content, _ = await self.llm_emotion_judge.generate_response_async(prompt, temperature=1.5)
- logger.info(f"[情感] 表情包情感描述: {content}")
- return content
-
- except Exception as e:
- logger.error(f"[错误] 获取表情包情感失败: {str(e)}")
- return None
-
- async def scan_new_emojis(self):
- """扫描新的表情包"""
- try:
- emoji_dir = self.EMOJI_DIR
- os.makedirs(emoji_dir, exist_ok=True)
-
- # 获取所有支持的图片文件
- files_to_process = [
- f for f in os.listdir(emoji_dir) if f.lower().endswith((".jpg", ".jpeg", ".png", ".gif"))
- ]
-
- # 检查当前表情包数量
- self._update_emoji_count()
- if self.emoji_num >= self.emoji_num_max:
- logger.warning(f"[警告] 表情包数量已达到上限({self.emoji_num}/{self.emoji_num_max}),跳过注册")
- return
-
- # 计算还可以注册的数量
- remaining_slots = self.emoji_num_max - self.emoji_num
- logger.info(f"[注册] 还可以注册 {remaining_slots} 个表情包")
-
- for filename in files_to_process:
- # 如果已经达到上限,停止注册
- if self.emoji_num >= self.emoji_num_max:
- logger.warning(f"[警告] 表情包数量已达到上限({self.emoji_num}/{self.emoji_num_max}),停止注册")
- break
-
- image_path = os.path.join(emoji_dir, filename)
-
- # 获取图片的base64编码和哈希值
- image_base64 = image_path_to_base64(image_path)
- if image_base64 is None:
- os.remove(image_path)
- continue
-
- image_bytes = base64.b64decode(image_base64)
- image_hash = hashlib.md5(image_bytes).hexdigest()
- image_format = Image.open(io.BytesIO(image_bytes)).format.lower()
- # 检查是否已经注册过
- existing_emoji_by_path = db["emoji"].find_one({"filename": filename})
- existing_emoji_by_hash = db["emoji"].find_one({"hash": image_hash})
- if existing_emoji_by_path and existing_emoji_by_hash:
- if existing_emoji_by_path["_id"] != existing_emoji_by_hash["_id"]:
- logger.error(f"[错误] 表情包已存在但记录不一致: {filename}")
- db.emoji.delete_one({"_id": existing_emoji_by_path["_id"]})
- db.emoji.delete_one({"_id": existing_emoji_by_hash["_id"]})
- existing_emoji = None
- else:
- existing_emoji = existing_emoji_by_hash
- elif existing_emoji_by_hash:
- logger.error(f"[错误] 表情包hash已存在但path不存在: {filename}")
- db.emoji.delete_one({"_id": existing_emoji_by_hash["_id"]})
- existing_emoji = None
- elif existing_emoji_by_path:
- logger.error(f"[错误] 表情包path已存在但hash不存在: {filename}")
- db.emoji.delete_one({"_id": existing_emoji_by_path["_id"]})
- existing_emoji = None
- else:
- existing_emoji = None
-
- description = None
-
- if existing_emoji:
- # 即使表情包已存在,也检查是否需要同步到images集合
- description = existing_emoji.get("description")
- # 检查是否在images集合中存在
- existing_image = db.images.find_one({"hash": image_hash})
- if not existing_image:
- # 同步到images集合
- image_doc = {
- "hash": image_hash,
- "path": image_path,
- "type": "emoji",
- "description": description,
- "timestamp": int(time.time()),
- }
- db.images.update_one({"hash": image_hash}, {"$set": image_doc}, upsert=True)
- # 保存描述到image_descriptions集合
- image_manager._save_description_to_db(image_hash, description, "emoji")
- logger.success(f"[同步] 已同步表情包到images集合: {filename}")
- continue
-
- # 检查是否在images集合中已有描述
- existing_description = image_manager._get_description_from_db(image_hash, "emoji")
-
- if existing_description:
- description = existing_description
- else:
- # 获取表情包的描述
- description = await self._get_emoji_description(image_base64)
-
- if global_config.EMOJI_CHECK:
- check = await self._check_emoji(image_base64, image_format)
- if "是" not in check:
- os.remove(image_path)
- logger.info(f"[过滤] 表情包描述: {description}")
- logger.info(f"[过滤] 表情包不满足规则,已移除: {check}")
- continue
- logger.info(f"[检查] 表情包检查通过: {check}")
-
- if description is not None:
- embedding = await get_embedding(description, request_type="emoji")
- if not embedding:
- logger.error("获取消息嵌入向量失败")
- raise ValueError("获取消息嵌入向量失败")
- # 准备数据库记录
- emoji_record = {
- "filename": filename,
- "path": image_path,
- "embedding": embedding,
- "description": description,
- "hash": image_hash,
- "timestamp": int(time.time()),
- }
-
- # 保存到emoji数据库
- db["emoji"].insert_one(emoji_record)
- logger.success(f"[注册] 新表情包: {filename}")
- logger.info(f"[描述] {description}")
-
- # 更新当前表情包数量
- self.emoji_num += 1
- logger.info(f"[统计] 当前表情包数量: {self.emoji_num}/{self.emoji_num_max}")
-
- # 保存到images数据库
- image_doc = {
- "hash": image_hash,
- "path": image_path,
- "type": "emoji",
- "description": description,
- "timestamp": int(time.time()),
- }
- db.images.update_one({"hash": image_hash}, {"$set": image_doc}, upsert=True)
- # 保存描述到image_descriptions集合
- image_manager._save_description_to_db(image_hash, description, "emoji")
- logger.success(f"[同步] 已保存到images集合: {filename}")
- else:
- logger.warning(f"[跳过] 表情包: {filename}")
-
- except Exception:
- logger.exception("[错误] 扫描表情包失败")
-
- def check_emoji_file_integrity(self):
- """检查表情包文件完整性
- 如果文件已被删除,则从数据库中移除对应记录
- """
- try:
- self._ensure_db()
- # 获取所有表情包记录
- all_emojis = list(db.emoji.find())
- removed_count = 0
- total_count = len(all_emojis)
-
- for emoji in all_emojis:
- try:
- if "path" not in emoji:
- logger.warning(f"[检查] 发现无效记录(缺少path字段),ID: {emoji.get('_id', 'unknown')}")
- db.emoji.delete_one({"_id": emoji["_id"]})
- removed_count += 1
- continue
-
- if "embedding" not in emoji:
- logger.warning(f"[检查] 发现过时记录(缺少embedding字段),ID: {emoji.get('_id', 'unknown')}")
- db.emoji.delete_one({"_id": emoji["_id"]})
- removed_count += 1
- continue
-
- # 检查文件是否存在
- if not os.path.exists(emoji["path"]):
- logger.warning(f"[检查] 表情包文件已被删除: {emoji['path']}")
- # 从数据库中删除记录
- result = db.emoji.delete_one({"_id": emoji["_id"]})
- if result.deleted_count > 0:
- logger.debug(f"[清理] 成功删除数据库记录: {emoji['_id']}")
- removed_count += 1
- else:
- logger.error(f"[错误] 删除数据库记录失败: {emoji['_id']}")
- continue
-
- if "hash" not in emoji:
- logger.warning(f"[检查] 发现缺失记录(缺少hash字段),ID: {emoji.get('_id', 'unknown')}")
- hash = hashlib.md5(open(emoji["path"], "rb").read()).hexdigest()
- db.emoji.update_one({"_id": emoji["_id"]}, {"$set": {"hash": hash}})
- else:
- file_hash = hashlib.md5(open(emoji["path"], "rb").read()).hexdigest()
- if emoji["hash"] != file_hash:
- logger.warning(f"[检查] 表情包文件hash不匹配,ID: {emoji.get('_id', 'unknown')}")
- db.emoji.delete_one({"_id": emoji["_id"]})
- removed_count += 1
-
- # 修复拼写错误
- if "discription" in emoji:
- desc = emoji["discription"]
- db.emoji.update_one(
- {"_id": emoji["_id"]}, {"$unset": {"discription": ""}, "$set": {"description": desc}}
- )
-
- except Exception as item_error:
- logger.error(f"[错误] 处理表情包记录时出错: {str(item_error)}")
- continue
-
- # 验证清理结果
- remaining_count = db.emoji.count_documents({})
- if removed_count > 0:
- logger.success(f"[清理] 已清理 {removed_count} 个失效的表情包记录")
- logger.info(f"[统计] 清理前: {total_count} | 清理后: {remaining_count}")
- else:
- logger.info(f"[检查] 已检查 {total_count} 个表情包记录")
-
- except Exception as e:
- logger.error(f"[错误] 检查表情包完整性失败: {str(e)}")
- logger.error(traceback.format_exc())
-
- def check_emoji_file_full(self):
- """检查表情包文件是否完整,如果数量超出限制且允许删除,则删除多余的表情包
-
- 删除规则:
- 1. 优先删除创建时间更早的表情包
- 2. 优先删除使用次数少的表情包,但使用次数多的也有小概率被删除
- """
- try:
- self._ensure_db()
- # 更新表情包数量
- self._update_emoji_count()
-
- # 检查是否超出限制
- if self.emoji_num <= self.emoji_num_max:
- return
-
- # 如果超出限制但不允许删除,则只记录警告
- if not global_config.max_reach_deletion:
- logger.warning(f"[警告] 表情包数量({self.emoji_num})超出限制({self.emoji_num_max}),但未开启自动删除")
- return
-
- # 计算需要删除的数量
- delete_count = self.emoji_num - self.emoji_num_max
- logger.info(f"[清理] 需要删除 {delete_count} 个表情包")
-
- # 获取所有表情包,按时间戳升序(旧的在前)排序
- all_emojis = list(db.emoji.find().sort([("timestamp", 1)]))
-
- # 计算权重:使用次数越多,被删除的概率越小
- weights = []
- max_usage = max((emoji.get("usage_count", 0) for emoji in all_emojis), default=1)
- for emoji in all_emojis:
- usage_count = emoji.get("usage_count", 0)
- # 使用指数衰减函数计算权重,使用次数越多权重越小
- weight = 1.0 / (1.0 + usage_count / max(1, max_usage))
- weights.append(weight)
-
- # 根据权重随机选择要删除的表情包
- to_delete = []
- remaining_indices = list(range(len(all_emojis)))
-
- while len(to_delete) < delete_count and remaining_indices:
- # 计算当前剩余表情包的权重
- current_weights = [weights[i] for i in remaining_indices]
- # 归一化权重
- total_weight = sum(current_weights)
- if total_weight == 0:
- break
- normalized_weights = [w / total_weight for w in current_weights]
-
- # 随机选择一个表情包
- selected_idx = random.choices(remaining_indices, weights=normalized_weights, k=1)[0]
- to_delete.append(all_emojis[selected_idx])
- remaining_indices.remove(selected_idx)
-
- # 删除选中的表情包
- deleted_count = 0
- for emoji in to_delete:
- try:
- # 删除文件
- if "path" in emoji and os.path.exists(emoji["path"]):
- os.remove(emoji["path"])
- logger.info(f"[删除] 文件: {emoji['path']} (使用次数: {emoji.get('usage_count', 0)})")
-
- # 删除数据库记录
- db.emoji.delete_one({"_id": emoji["_id"]})
- deleted_count += 1
-
- # 同时从images集合中删除
- if "hash" in emoji:
- db.images.delete_one({"hash": emoji["hash"]})
-
- except Exception as e:
- logger.error(f"[错误] 删除表情包失败: {str(e)}")
- continue
-
- # 更新表情包数量
- self._update_emoji_count()
- logger.success(f"[清理] 已删除 {deleted_count} 个表情包,当前数量: {self.emoji_num}")
-
- except Exception as e:
- logger.error(f"[错误] 检查表情包数量失败: {str(e)}")
-
- async def start_periodic_check_register(self):
- """定期检查表情包完整性和数量"""
- while True:
- logger.info("[扫描] 开始检查表情包完整性...")
- self.check_emoji_file_integrity()
- logger.info("[扫描] 开始删除所有图片缓存...")
- await self.delete_all_images()
- logger.info("[扫描] 开始扫描新表情包...")
- if self.emoji_num < self.emoji_num_max:
- await self.scan_new_emojis()
- if self.emoji_num > self.emoji_num_max:
- logger.warning(f"[警告] 表情包数量超过最大限制: {self.emoji_num} > {self.emoji_num_max},跳过注册")
- if not global_config.max_reach_deletion:
- logger.warning("表情包数量超过最大限制,终止注册")
- break
- else:
- logger.warning("表情包数量超过最大限制,开始删除表情包")
- self.check_emoji_file_full()
- await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60)
-
- @staticmethod
- async def delete_all_images():
- """删除 data/image 目录下的所有文件"""
- try:
- image_dir = os.path.join("data", "image")
- if not os.path.exists(image_dir):
- logger.warning(f"[警告] 目录不存在: {image_dir}")
- return
-
- deleted_count = 0
- failed_count = 0
-
- # 遍历目录下的所有文件
- for filename in os.listdir(image_dir):
- file_path = os.path.join(image_dir, filename)
- try:
- if os.path.isfile(file_path):
- os.remove(file_path)
- deleted_count += 1
- logger.debug(f"[删除] 文件: {file_path}")
- except Exception as e:
- failed_count += 1
- logger.error(f"[错误] 删除文件失败 {file_path}: {str(e)}")
-
- logger.success(f"[清理] 已删除 {deleted_count} 个文件,失败 {failed_count} 个")
-
- except Exception as e:
- logger.error(f"[错误] 删除图片目录失败: {str(e)}")
-
-
-# 创建全局单例
-emoji_manager = EmojiManager()
diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py
index 2ba645f9..c7f7ac83 100644
--- a/src/plugins/chat/message.py
+++ b/src/plugins/chat/message.py
@@ -7,7 +7,7 @@ import urllib3
from src.common.logger import get_module_logger
from .chat_stream import ChatStream
from .utils_image import image_manager
-from ..message.message_base import Seg, UserInfo, BaseMessageInfo, MessageBase
+from maim_message import Seg, UserInfo, BaseMessageInfo, MessageBase
logger = get_module_logger("chat_message")
@@ -127,12 +127,12 @@ class MessageRecv(Message):
# 如果是base64图片数据
if isinstance(seg.data, str):
return await image_manager.get_image_description(seg.data)
- return "[图片]"
+ return "[发了一张图片,网卡了加载不出来]"
elif seg.type == "emoji":
self.is_emoji = True
if isinstance(seg.data, str):
return await image_manager.get_emoji_description(seg.data)
- return "[表情]"
+ return "[发了一个表情包,网卡了加载不出来]"
else:
return f"[{seg.type}:{str(seg.data)}]"
except Exception as e:
@@ -141,14 +141,8 @@ class MessageRecv(Message):
def _generate_detailed_text(self) -> str:
"""生成详细文本,包含时间和用户信息"""
- # time_str = time.strftime("%m-%d %H:%M:%S", time.localtime(self.message_info.time))
timestamp = self.message_info.time
user_info = self.message_info.user_info
- # name = (
- # f"{user_info.user_nickname}(ta的昵称:{user_info.user_cardname},ta的id:{user_info.user_id})"
- # if user_info.user_cardname != None
- # else f"{user_info.user_nickname}(ta的id:{user_info.user_id})"
- # )
name = f"<{self.message_info.platform}:{user_info.user_id}:{user_info.user_nickname}:{user_info.user_cardname}>"
return f"[{timestamp}] {name}: {self.processed_plain_text}\n"
@@ -222,11 +216,11 @@ class MessageProcessBase(Message):
# 如果是base64图片数据
if isinstance(seg.data, str):
return await image_manager.get_image_description(seg.data)
- return "[图片]"
+ return "[图片,网卡了加载不出来]"
elif seg.type == "emoji":
if isinstance(seg.data, str):
return await image_manager.get_emoji_description(seg.data)
- return "[表情]"
+ return "[表情,网卡了加载不出来]"
elif seg.type == "at":
return f"[@{seg.data}]"
elif seg.type == "reply":
diff --git a/src/plugins/chat/message_buffer.py b/src/plugins/chat/message_buffer.py
index d0ab5604..d76d2328 100644
--- a/src/plugins/chat/message_buffer.py
+++ b/src/plugins/chat/message_buffer.py
@@ -3,7 +3,7 @@ from src.common.logger import get_module_logger
import asyncio
from dataclasses import dataclass, field
from .message import MessageRecv
-from ..message.message_base import BaseMessageInfo, GroupInfo, Seg
+from maim_message import BaseMessageInfo, GroupInfo
import hashlib
from typing import Dict
from collections import OrderedDict
@@ -128,58 +128,67 @@ class MessageBuffer:
if result:
async with self.lock: # 再次加锁
# 清理所有早于当前消息的已处理消息, 收集所有早于当前消息的F消息的processed_plain_text
- keep_msgs = OrderedDict()
- combined_text = []
- found = False
- type = "seglist"
- is_update = True
- for msg_id, msg in self.buffer_pool[person_id_].items():
+ keep_msgs = OrderedDict() # 用于存放 T 消息之后的消息
+ collected_texts = [] # 用于收集 T 消息及之前 F 消息的文本
+ process_target_found = False
+
+ # 遍历当前用户的所有缓冲消息
+ for msg_id, cache_msg in self.buffer_pool[person_id_].items():
+ # 如果找到了目标处理消息 (T 状态)
if msg_id == message.message_info.message_id:
- found = True
- if msg.message.message_segment.type != "seglist":
- type = msg.message.message_segment.type
- else:
- if (
- isinstance(msg.message.message_segment.data, list)
- and all(isinstance(x, Seg) for x in msg.message.message_segment.data)
- and len(msg.message.message_segment.data) == 1
- ):
- type = msg.message.message_segment.data[0].type
- combined_text.append(msg.message.processed_plain_text)
- continue
- if found:
- keep_msgs[msg_id] = msg
- elif msg.result == "F":
- # 收集F消息的文本内容
- f_type = "seglist"
- if msg.message.message_segment.type != "seglist":
- f_type = msg.message.message_segment.type
- else:
- if (
- isinstance(msg.message.message_segment.data, list)
- and all(isinstance(x, Seg) for x in msg.message.message_segment.data)
- and len(msg.message.message_segment.data) == 1
- ):
- f_type = msg.message.message_segment.data[0].type
- if hasattr(msg.message, "processed_plain_text") and msg.message.processed_plain_text:
- if f_type == "text":
- combined_text.append(msg.message.processed_plain_text)
- elif f_type != "text":
- is_update = False
- elif msg.result == "U":
- logger.debug(f"异常未处理信息id: {msg.message.message_info.message_id}")
+ process_target_found = True
+ # 收集这条 T 消息的文本 (如果有)
+ if (
+ hasattr(cache_msg.message, "processed_plain_text")
+ and cache_msg.message.processed_plain_text
+ ):
+ collected_texts.append(cache_msg.message.processed_plain_text)
+ # 不立即放入 keep_msgs,因为它之前的 F 消息也处理完了
- # 更新当前消息的processed_plain_text
- if combined_text and combined_text[0] != message.processed_plain_text and is_update:
- if type == "text":
- message.processed_plain_text = ",".join(combined_text)
- logger.debug(f"整合了{len(combined_text) - 1}条F消息的内容到当前消息")
- elif type == "emoji":
- combined_text.pop()
- message.processed_plain_text = ",".join(combined_text)
- message.is_emoji = False
- logger.debug(f"整合了{len(combined_text) - 1}条F消息的内容,覆盖当前emoji消息")
+ # 如果已经找到了目标 T 消息,之后的消息需要保留
+ elif process_target_found:
+ keep_msgs[msg_id] = cache_msg
+ # 如果还没找到目标 T 消息,说明是之前的消息 (F 或 U)
+ else:
+ if cache_msg.result == "F":
+ # 收集这条 F 消息的文本 (如果有)
+ if (
+ hasattr(cache_msg.message, "processed_plain_text")
+ and cache_msg.message.processed_plain_text
+ ):
+ collected_texts.append(cache_msg.message.processed_plain_text)
+ elif cache_msg.result == "U":
+ # 理论上不应该在 T 消息之前还有 U 消息,记录日志
+ logger.warning(
+ f"异常状态:在目标 T 消息 {message.message_info.message_id} 之前发现未处理的 U 消息 {cache_msg.message.message_info.message_id}"
+ )
+ # 也可以选择收集其文本
+ if (
+ hasattr(cache_msg.message, "processed_plain_text")
+ and cache_msg.message.processed_plain_text
+ ):
+ collected_texts.append(cache_msg.message.processed_plain_text)
+
+ # 更新当前消息 (message) 的 processed_plain_text
+ # 只有在收集到的文本多于一条,或者只有一条但与原始文本不同时才合并
+ if collected_texts:
+ # 使用 OrderedDict 去重,同时保留原始顺序
+ unique_texts = list(OrderedDict.fromkeys(collected_texts))
+ merged_text = ",".join(unique_texts)
+
+ # 只有在合并后的文本与原始文本不同时才更新
+ # 并且确保不是空合并
+ if merged_text and merged_text != message.processed_plain_text:
+ message.processed_plain_text = merged_text
+ # 如果合并了文本,原消息不再视为纯 emoji
+ if hasattr(message, "is_emoji"):
+ message.is_emoji = False
+ logger.debug(
+ f"合并了 {len(unique_texts)} 条消息的文本内容到当前消息 {message.message_info.message_id}"
+ )
+
+ # 更新缓冲池,只保留 T 消息之后的消息
self.buffer_pool[person_id_] = keep_msgs
return result
except asyncio.TimeoutError:
diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py
index a737d99c..d51492f7 100644
--- a/src/plugins/chat/message_sender.py
+++ b/src/plugins/chat/message_sender.py
@@ -62,20 +62,10 @@ class MessageSender:
# logger.trace(f"{message.processed_plain_text},{typing_time},等待输入时间结束") # 减少日志
# --- 结束打字延迟 ---
- message_json = message.to_dict()
message_preview = truncate_message(message.processed_plain_text)
try:
- end_point = global_config.api_urls.get(message.message_info.platform, None)
- if end_point:
- try:
- await global_api.send_message_rest(end_point, message_json)
- except Exception as e:
- logger.error(f"REST发送失败: {str(e)}")
- logger.info(f"[{message.chat_stream.stream_id}] 尝试使用WS发送")
- await self.send_via_ws(message)
- else:
- await self.send_via_ws(message)
+ await self.send_via_ws(message)
logger.success(f"发送消息 '{message_preview}' 成功") # 调整日志格式
except Exception as e:
logger.error(f"发送消息 '{message_preview}' 失败: {str(e)}")
diff --git a/src/plugins/chat/only_message_process.py b/src/plugins/chat/only_message_process.py
index 5c239fb1..9009ffb1 100644
--- a/src/plugins/chat/only_message_process.py
+++ b/src/plugins/chat/only_message_process.py
@@ -62,4 +62,6 @@ class MessageProcessor:
mes_name = chat.group_info.group_name if chat.group_info else "私聊"
# 将时间戳转换为datetime对象
current_time = datetime.fromtimestamp(message.message_info.time).strftime("%H:%M:%S")
- logger.info(f"[{current_time}][{mes_name}]{chat.user_info.user_nickname}: {message.processed_plain_text}")
+ logger.info(
+ f"[{current_time}][{mes_name}]{message.message_info.user_info.user_nickname}: {message.processed_plain_text}"
+ )
diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py
index 271386ff..4eb3d93d 100644
--- a/src/plugins/chat/utils.py
+++ b/src/plugins/chat/utils.py
@@ -12,7 +12,7 @@ from ..models.utils_model import LLMRequest
from ..utils.typo_generator import ChineseTypoGenerator
from ...config.config import global_config
from .message import MessageRecv, Message
-from ..message.message_base import UserInfo
+from maim_message import UserInfo
from .chat_stream import ChatStream
from ..moods.moods import MoodManager
from ...common.database import db
@@ -234,6 +234,13 @@ def split_into_sentences_w_remove_punctuation(text: str) -> List[str]:
Returns:
List[str]: 分割和合并后的句子列表
"""
+ # 预处理:处理多余的换行符
+ # 1. 将连续的换行符替换为单个换行符
+ text = re.sub(r"\n\s*\n+", "\n", text)
+ # 2. 处理换行符和其他分隔符的组合
+ text = re.sub(r"\n\s*([,,。;\s])", r"\1", text)
+ text = re.sub(r"([,,。;\s])\s*\n", r"\1", text)
+
# 处理两个汉字中间的换行符
text = re.sub(r"([\u4e00-\u9fff])\n([\u4e00-\u9fff])", r"\1。\2", text)
@@ -327,8 +334,10 @@ def split_into_sentences_w_remove_punctuation(text: str) -> List[str]:
# 提取最终的句子内容
final_sentences = [content for content, sep in merged_segments if content] # 只保留有内容的段
- # 清理可能引入的空字符串
- final_sentences = [s for s in final_sentences if s]
+ # 清理可能引入的空字符串和仅包含空白的字符串
+ final_sentences = [
+ s for s in final_sentences if s.strip()
+ ] # 过滤掉空字符串以及仅包含空白(如换行符、空格)的字符串
logger.debug(f"分割并合并后的句子: {final_sentences}")
return final_sentences
@@ -363,12 +372,16 @@ def random_remove_punctuation(text: str) -> str:
def process_llm_response(text: str) -> List[str]:
# 先保护颜文字
- protected_text, kaomoji_mapping = protect_kaomoji(text)
- logger.trace(f"保护颜文字后的文本: {protected_text}")
+ if global_config.enable_kaomoji_protection:
+ protected_text, kaomoji_mapping = protect_kaomoji(text)
+ logger.trace(f"保护颜文字后的文本: {protected_text}")
+ else:
+ protected_text = text
+ kaomoji_mapping = {}
# 提取被 () 或 [] 包裹且包含中文的内容
pattern = re.compile(r"[\(\[\(](?=.*[\u4e00-\u9fff]).*?[\)\]\)]")
# _extracted_contents = pattern.findall(text)
- extracted_contents = pattern.findall(protected_text) # 在保护后的文本上查找
+ _extracted_contents = pattern.findall(protected_text) # 在保护后的文本上查找
# 去除 () 和 [] 及其包裹的内容
cleaned_text = pattern.sub("", protected_text)
@@ -411,13 +424,14 @@ def process_llm_response(text: str) -> List[str]:
if len(sentences) > max_sentence_num:
logger.warning(f"分割后消息数量过多 ({len(sentences)} 条),返回默认回复")
return [f"{global_config.BOT_NICKNAME}不知道哦"]
- if extracted_contents:
- for content in extracted_contents:
- sentences.append(content)
- # 在所有句子处理完毕后,对包含占位符的列表进行恢复
- sentences = recover_kaomoji(sentences, kaomoji_mapping)
- print(sentences)
+ # if extracted_contents:
+ # for content in extracted_contents:
+ # sentences.append(content)
+
+ # 在所有句子处理完毕后,对包含占位符的列表进行恢复
+ if global_config.enable_kaomoji_protection:
+ sentences = recover_kaomoji(sentences, kaomoji_mapping)
return sentences
diff --git a/src/plugins/chat/utils_image.py b/src/plugins/chat/utils_image.py
index 9c7a03b0..f6b9231a 100644
--- a/src/plugins/chat/utils_image.py
+++ b/src/plugins/chat/utils_image.py
@@ -121,7 +121,7 @@ class ImageManager:
prompt = "这是一个动态图表情包,每一张图代表了动态图的某一帧,黑色背景代表透明,使用1-2个词描述一下表情包表达的情感和内容,简短一些"
description, _ = await self._llm.generate_response_for_image(prompt, image_base64, "jpg")
else:
- prompt = "这是一个表情包,请用使用1-2个词描述一下表情包所表达的情感和内容,简短一些"
+ prompt = "这是一个表情包,请用使用几个词描述一下表情包所表达的情感和内容,简短一些"
description, _ = await self._llm.generate_response_for_image(prompt, image_base64, image_format)
cached_description = self._get_description_from_db(image_hash, "emoji")
@@ -130,7 +130,7 @@ class ImageManager:
return f"[表达了:{cached_description}]"
# 根据配置决定是否保存图片
- if global_config.EMOJI_SAVE:
+ if global_config.save_emoji:
# 生成文件名和路径
timestamp = int(time.time())
filename = f"{timestamp}_{image_hash[:8]}.{image_format}"
@@ -152,7 +152,7 @@ class ImageManager:
"timestamp": timestamp,
}
db.images.update_one({"hash": image_hash}, {"$set": image_doc}, upsert=True)
- logger.success(f"保存表情包: {file_path}")
+ logger.trace(f"保存表情包: {file_path}")
except Exception as e:
logger.error(f"保存表情包文件失败: {str(e)}")
@@ -196,7 +196,7 @@ class ImageManager:
return "[图片]"
# 根据配置决定是否保存图片
- if global_config.EMOJI_SAVE:
+ if global_config.save_pic:
# 生成文件名和路径
timestamp = int(time.time())
filename = f"{timestamp}_{image_hash[:8]}.{image_format}"
@@ -309,11 +309,15 @@ def image_path_to_base64(image_path: str) -> str:
image_path: 图片文件路径
Returns:
str: base64编码的图片数据
+ Raises:
+ FileNotFoundError: 当图片文件不存在时
+ IOError: 当读取图片文件失败时
"""
- try:
- with open(image_path, "rb") as f:
- image_data = f.read()
- return base64.b64encode(image_data).decode("utf-8")
- except Exception as e:
- logger.error(f"读取图片失败: {image_path}, 错误: {str(e)}")
- return None
+ if not os.path.exists(image_path):
+ raise FileNotFoundError(f"图片文件不存在: {image_path}")
+
+ with open(image_path, "rb") as f:
+ image_data = f.read()
+ if not image_data:
+ raise IOError(f"读取图片文件失败: {image_path}")
+ return base64.b64encode(image_data).decode("utf-8")
diff --git a/src/plugins/emoji_system/emoji_manager.py b/src/plugins/emoji_system/emoji_manager.py
new file mode 100644
index 00000000..cf3ebadb
--- /dev/null
+++ b/src/plugins/emoji_system/emoji_manager.py
@@ -0,0 +1,827 @@
+import asyncio
+import base64
+import hashlib
+import os
+import random
+import time
+import traceback
+from typing import Optional, Tuple
+from PIL import Image
+import io
+import re
+
+from ...common.database import db
+from ...config.config import global_config
+from ..chat.utils_image import image_path_to_base64, image_manager
+from ..models.utils_model import LLMRequest
+from src.common.logger import get_module_logger, LogConfig, EMOJI_STYLE_CONFIG
+
+
+emoji_log_config = LogConfig(
+ console_format=EMOJI_STYLE_CONFIG["console_format"],
+ file_format=EMOJI_STYLE_CONFIG["file_format"],
+)
+
+logger = get_module_logger("emoji", config=emoji_log_config)
+
+BASE_DIR = os.path.join("data")
+EMOJI_DIR = os.path.join(BASE_DIR, "emoji") # 表情包存储目录
+EMOJI_REGISTED_DIR = os.path.join(BASE_DIR, "emoji_registed") # 已注册的表情包注册目录
+
+
+"""
+还没经过测试,有些地方数据库和内存数据同步可能不完全
+
+"""
+
+
+class MaiEmoji:
+ """定义一个表情包"""
+
+ def __init__(self, filename: str, path: str):
+ self.path = path # 存储目录路径
+ self.filename = filename
+ self.embedding = []
+ self.hash = "" # 初始为空,在创建实例时会计算
+ self.description = ""
+ self.emotion = []
+ self.usage_count = 0
+ self.last_used_time = time.time()
+ self.register_time = time.time()
+ self.is_deleted = False # 标记是否已被删除
+ self.format = ""
+
+ async def initialize_hash_format(self):
+ """从文件创建表情包实例
+
+ 参数:
+ file_path: 文件的完整路径
+
+ 返回:
+ MaiEmoji: 创建的表情包实例,如果失败则返回None
+ """
+ try:
+ file_path = os.path.join(self.path, self.filename)
+ if not os.path.exists(file_path):
+ logger.error(f"[错误] 表情包文件不存在: {file_path}")
+ return None
+
+ image_base64 = image_path_to_base64(file_path)
+ if image_base64 is None:
+ logger.error(f"[错误] 无法读取图片: {file_path}")
+ return None
+
+ # 计算哈希值
+ image_bytes = base64.b64decode(image_base64)
+ self.hash = hashlib.md5(image_bytes).hexdigest()
+
+ # 获取图片格式
+ self.format = Image.open(io.BytesIO(image_bytes)).format.lower()
+
+ except Exception as e:
+ logger.error(f"[错误] 初始化表情包失败: {str(e)}")
+ logger.error(traceback.format_exc())
+ return None
+
+ async def register_to_db(self):
+ """
+ 注册表情包
+ 将表情包对应的文件,从当前路径移动到EMOJI_REGISTED_DIR目录下
+ 并修改对应的实例属性,然后将表情包信息保存到数据库中
+ """
+ try:
+ # 确保目标目录存在
+ os.makedirs(EMOJI_REGISTED_DIR, exist_ok=True)
+
+ # 源路径是当前实例的完整路径
+ source_path = os.path.join(self.path, self.filename)
+ # 目标路径
+ destination_path = os.path.join(EMOJI_REGISTED_DIR, self.filename)
+
+ # 检查源文件是否存在
+ if not os.path.exists(source_path):
+ logger.error(f"[错误] 源文件不存在: {source_path}")
+ return False
+
+ # --- 文件移动 ---
+ try:
+ # 如果目标文件已存在,先删除 (确保移动成功)
+ if os.path.exists(destination_path):
+ os.remove(destination_path)
+
+ os.rename(source_path, destination_path)
+ logger.info(f"[移动] 文件从 {source_path} 移动到 {destination_path}")
+ # 更新实例的路径属性为新目录
+ self.path = EMOJI_REGISTED_DIR
+ except Exception as move_error:
+ logger.error(f"[错误] 移动文件失败: {str(move_error)}")
+ return False # 文件移动失败,不继续
+
+ # --- 数据库操作 ---
+ try:
+ # 准备数据库记录 for emoji collection
+ emoji_record = {
+ "filename": self.filename,
+ "path": os.path.join(self.path, self.filename), # 使用更新后的路径
+ "embedding": self.embedding,
+ "description": self.description,
+ "emotion": self.emotion, # 添加情感标签字段
+ "hash": self.hash,
+ "format": self.format,
+ "timestamp": int(self.register_time), # 使用实例的注册时间
+ "usage_count": self.usage_count,
+ "last_used_time": self.last_used_time,
+ }
+
+ # 使用upsert确保记录存在或被更新
+ db["emoji"].update_one({"hash": self.hash}, {"$set": emoji_record}, upsert=True)
+ logger.success(f"[注册] 表情包信息保存到数据库: {self.description}")
+
+ return True
+
+ except Exception as db_error:
+ logger.error(f"[错误] 保存数据库失败: {str(db_error)}")
+ # 考虑是否需要将文件移回?为了简化,暂时只记录错误
+ return False
+
+ except Exception as e:
+ logger.error(f"[错误] 注册表情包失败: {str(e)}")
+ logger.error(traceback.format_exc())
+ return False
+
+ async def delete(self):
+ """删除表情包
+
+ 删除表情包的文件和数据库记录
+
+ 返回:
+ bool: 是否成功删除
+ """
+ try:
+ # 1. 删除文件
+ if os.path.exists(os.path.join(self.path, self.filename)):
+ try:
+ os.remove(os.path.join(self.path, self.filename))
+ logger.info(f"[删除] 文件: {os.path.join(self.path, self.filename)}")
+ except Exception as e:
+ logger.error(f"[错误] 删除文件失败 {os.path.join(self.path, self.filename)}: {str(e)}")
+ # 继续执行,即使文件删除失败也尝试删除数据库记录
+
+ # 2. 删除数据库记录
+ result = db.emoji.delete_one({"hash": self.hash})
+ deleted_in_db = result.deleted_count > 0
+
+ if deleted_in_db:
+ logger.success(f"[删除] 成功删除表情包记录: {self.description}")
+
+ # 3. 标记对象已被删除
+ self.is_deleted = True
+ return True
+ else:
+ logger.error(f"[错误] 删除表情包记录失败: {self.hash}")
+ return False
+
+ except Exception as e:
+ logger.error(f"[错误] 删除表情包失败: {str(e)}")
+ return False
+
+
+class EmojiManager:
+ _instance = None
+
+ def __new__(cls):
+ if cls._instance is None:
+ cls._instance = super().__new__(cls)
+ cls._instance._initialized = False
+ return cls._instance
+
+ def __init__(self):
+ self._scan_task = None
+ self.vlm = LLMRequest(model=global_config.vlm, temperature=0.3, max_tokens=1000, request_type="emoji")
+ self.llm_emotion_judge = LLMRequest(
+ model=global_config.llm_emotion_judge, max_tokens=600, temperature=0.8, request_type="emoji"
+ ) # 更高的温度,更少的token(后续可以根据情绪来调整温度)
+
+ self.emoji_num = 0
+ self.emoji_num_max = global_config.max_emoji_num
+ self.emoji_num_max_reach_deletion = global_config.max_reach_deletion
+ self.emoji_objects: list[MaiEmoji] = [] # 存储MaiEmoji对象的列表,使用类型注解明确列表元素类型
+
+ logger.info("启动表情包管理器")
+
+ def _ensure_emoji_dir(self):
+ """确保表情存储目录存在"""
+ os.makedirs(EMOJI_DIR, exist_ok=True)
+
+ def initialize(self):
+ """初始化数据库连接和表情目录"""
+ if not self._initialized:
+ try:
+ self._ensure_emoji_collection()
+ self._ensure_emoji_dir()
+ self._initialized = True
+ # 更新表情包数量
+ # 启动时执行一次完整性检查
+ # await self.check_emoji_file_integrity()
+ except Exception:
+ logger.exception("初始化表情管理器失败")
+
+ def _ensure_db(self):
+ """确保数据库已初始化"""
+ if not self._initialized:
+ self.initialize()
+ if not self._initialized:
+ raise RuntimeError("EmojiManager not initialized")
+
+ @staticmethod
+ def _ensure_emoji_collection():
+ """确保emoji集合存在并创建索引
+
+ 这个函数用于确保MongoDB数据库中存在emoji集合,并创建必要的索引。
+
+ 索引的作用是加快数据库查询速度:
+ - embedding字段的2dsphere索引: 用于加速向量相似度搜索,帮助快速找到相似的表情包
+ - tags字段的普通索引: 加快按标签搜索表情包的速度
+ - filename字段的唯一索引: 确保文件名不重复,同时加快按文件名查找的速度
+
+ 没有索引的话,数据库每次查询都需要扫描全部数据,建立索引后可以大大提高查询效率。
+ """
+ if "emoji" not in db.list_collection_names():
+ db.create_collection("emoji")
+ db.emoji.create_index([("embedding", "2dsphere")])
+ db.emoji.create_index([("filename", 1)], unique=True)
+
+ def record_usage(self, hash: str):
+ """记录表情使用次数"""
+ try:
+ db.emoji.update_one({"hash": hash}, {"$inc": {"usage_count": 1}})
+ for emoji in self.emoji_objects:
+ if emoji.hash == hash:
+ emoji.usage_count += 1
+ break
+
+ except Exception as e:
+ logger.error(f"记录表情使用失败: {str(e)}")
+
+ async def get_emoji_for_text(self, text_emotion: str) -> Optional[Tuple[str, str]]:
+ """根据文本内容获取相关表情包
+ Args:
+ text_emotion: 输入的情感描述文本
+ Returns:
+ Optional[Tuple[str, str]]: (表情包文件路径, 表情包描述),如果没有找到则返回None
+ """
+ try:
+ self._ensure_db()
+ time_start = time.time()
+
+ # 获取所有表情包
+ all_emojis = self.emoji_objects
+
+ if not all_emojis:
+ logger.warning("数据库中没有任何表情包")
+ return None
+
+ # 计算每个表情包与输入文本的最大情感相似度
+ emoji_similarities = []
+ for emoji in all_emojis:
+ emotions = emoji.emotion
+ if not emotions:
+ continue
+
+ # 计算与每个emotion标签的相似度,取最大值
+ max_similarity = 0
+ for emotion in emotions:
+ # 使用编辑距离计算相似度
+ distance = self._levenshtein_distance(text_emotion, emotion)
+ max_len = max(len(text_emotion), len(emotion))
+ similarity = 1 - (distance / max_len if max_len > 0 else 0)
+ max_similarity = max(max_similarity, similarity)
+
+ emoji_similarities.append((emoji, max_similarity))
+
+ # 按相似度降序排序
+ emoji_similarities.sort(key=lambda x: x[1], reverse=True)
+
+ # 获取前5个最相似的表情包
+ top_5_emojis = emoji_similarities[:10] if len(emoji_similarities) > 10 else emoji_similarities
+
+ if not top_5_emojis:
+ logger.warning("未找到匹配的表情包")
+ return None
+
+ # 从前5个中随机选择一个
+ selected_emoji, similarity = random.choice(top_5_emojis)
+
+ # 更新使用次数
+ self.record_usage(selected_emoji.hash)
+
+ time_end = time.time()
+
+ logger.info(
+ f"找到[{text_emotion}]表情包,用时:{time_end - time_start:.2f}秒: {selected_emoji.description} (相似度: {similarity:.4f})"
+ )
+ return selected_emoji.path, f"[ {selected_emoji.description} ]"
+
+ except Exception as e:
+ logger.error(f"[错误] 获取表情包失败: {str(e)}")
+ return None
+
+ def _levenshtein_distance(self, s1: str, s2: str) -> int:
+ """计算两个字符串的编辑距离
+
+ Args:
+ s1: 第一个字符串
+ s2: 第二个字符串
+
+ Returns:
+ int: 编辑距离
+ """
+ if len(s1) < len(s2):
+ return self._levenshtein_distance(s2, s1)
+
+ if len(s2) == 0:
+ return len(s1)
+
+ previous_row = range(len(s2) + 1)
+ for i, c1 in enumerate(s1):
+ current_row = [i + 1]
+ for j, c2 in enumerate(s2):
+ insertions = previous_row[j + 1] + 1
+ deletions = current_row[j] + 1
+ substitutions = previous_row[j] + (c1 != c2)
+ current_row.append(min(insertions, deletions, substitutions))
+ previous_row = current_row
+
+ return previous_row[-1]
+
+ async def check_emoji_file_integrity(self):
+ """检查表情包文件完整性
+ 遍历self.emoji_objects中的所有对象,检查文件是否存在
+ 如果文件已被删除,则执行对象的删除方法并从列表中移除
+ """
+ try:
+ if not self.emoji_objects:
+ logger.warning("[检查] emoji_objects为空,跳过完整性检查")
+ return
+
+ total_count = len(self.emoji_objects)
+ removed_count = 0
+ # 使用列表复制进行遍历,因为我们会在遍历过程中修改列表
+ for emoji in self.emoji_objects[:]:
+ try:
+ # 检查文件是否存在
+ if not os.path.exists(emoji.path):
+ logger.warning(f"[检查] 表情包文件已被删除: {emoji.path}")
+ # 执行表情包对象的删除方法
+ await emoji.delete()
+ # 从列表中移除该对象
+ self.emoji_objects.remove(emoji)
+ # 更新计数
+ self.emoji_num -= 1
+ removed_count += 1
+ continue
+
+ except Exception as item_error:
+ logger.error(f"[错误] 处理表情包记录时出错: {str(item_error)}")
+ continue
+
+ # 输出清理结果
+ if removed_count > 0:
+ logger.success(f"[清理] 已清理 {removed_count} 个失效的表情包记录")
+ logger.info(f"[统计] 清理前: {total_count} | 清理后: {len(self.emoji_objects)}")
+ else:
+ logger.info(f"[检查] 已检查 {total_count} 个表情包记录,全部完好")
+
+ except Exception as e:
+ logger.error(f"[错误] 检查表情包完整性失败: {str(e)}")
+ logger.error(traceback.format_exc())
+
+ async def start_periodic_check_register(self):
+ """定期检查表情包完整性和数量"""
+ await self.get_all_emoji_from_db()
+ while True:
+ logger.info("[扫描] 开始检查表情包完整性...")
+ await self.check_emoji_file_integrity()
+ await self.clear_temp_emoji()
+ logger.info("[扫描] 开始扫描新表情包...")
+
+ # 检查表情包目录是否存在
+ if not os.path.exists(EMOJI_DIR):
+ logger.warning(f"[警告] 表情包目录不存在: {EMOJI_DIR}")
+ os.makedirs(EMOJI_DIR, exist_ok=True)
+ logger.info(f"[创建] 已创建表情包目录: {EMOJI_DIR}")
+ await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60)
+ continue
+
+ # 检查目录是否为空
+ files = os.listdir(EMOJI_DIR)
+ if not files:
+ logger.warning(f"[警告] 表情包目录为空: {EMOJI_DIR}")
+ await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60)
+ continue
+
+ # 检查是否需要处理表情包(数量超过最大值或不足)
+ if (self.emoji_num > self.emoji_num_max and global_config.max_reach_deletion) or (
+ self.emoji_num < self.emoji_num_max
+ ):
+ try:
+ # 获取目录下所有图片文件
+ files_to_process = [
+ f
+ for f in files
+ if os.path.isfile(os.path.join(EMOJI_DIR, f))
+ and f.lower().endswith((".jpg", ".jpeg", ".png", ".gif"))
+ ]
+
+ # 处理每个符合条件的文件
+ for filename in files_to_process:
+ # 尝试注册表情包
+ success = await self.register_emoji_by_filename(filename)
+ if success:
+ # 注册成功则跳出循环
+ break
+ else:
+ # 注册失败则删除对应文件
+ file_path = os.path.join(EMOJI_DIR, filename)
+ os.remove(file_path)
+ logger.warning(f"[清理] 删除注册失败的表情包文件: {filename}")
+ except Exception as e:
+ logger.error(f"[错误] 扫描表情包目录失败: {str(e)}")
+
+ await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60)
+
+ async def get_all_emoji_from_db(self):
+ """获取所有表情包并初始化为MaiEmoji类对象
+
+ 参数:
+ hash: 可选,如果提供则只返回指定哈希值的表情包
+
+ 返回:
+ list[MaiEmoji]: 表情包对象列表
+ """
+ try:
+ self._ensure_db()
+
+ # 获取所有表情包
+ all_emoji_data = list(db.emoji.find())
+
+ # 将数据库记录转换为MaiEmoji对象
+ emoji_objects = []
+ for emoji_data in all_emoji_data:
+ emoji = MaiEmoji(
+ filename=emoji_data.get("filename", ""),
+ path=emoji_data.get("path", ""),
+ )
+
+ # 设置额外属性
+ emoji.hash = emoji_data.get("hash", "")
+ emoji.usage_count = emoji_data.get("usage_count", 0)
+ 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中
+ self.emoji_objects = emoji_objects
+
+ except Exception as e:
+ logger.error(f"[错误] 获取所有表情包对象失败: {str(e)}")
+
+ async def get_emoji_from_db(self, hash=None):
+ """获取所有表情包并初始化为MaiEmoji类对象
+
+ 参数:
+ hash: 可选,如果提供则只返回指定哈希值的表情包
+
+ 返回:
+ list[MaiEmoji]: 表情包对象列表
+ """
+ try:
+ self._ensure_db()
+
+ # 准备查询条件
+ query = {}
+ if hash:
+ query = {"hash": hash}
+
+ # 获取所有表情包
+ all_emoji_data = list(db.emoji.find(query))
+
+ # 将数据库记录转换为MaiEmoji对象
+ emoji_objects = []
+ for emoji_data in all_emoji_data:
+ emoji = MaiEmoji(
+ filename=emoji_data.get("filename", ""),
+ path=emoji_data.get("path", ""),
+ )
+
+ # 设置额外属性
+ emoji.usage_count = emoji_data.get("usage_count", 0)
+ 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中
+ self.emoji_objects = emoji_objects
+
+ return emoji_objects
+
+ except Exception as e:
+ logger.error(f"[错误] 获取所有表情包对象失败: {str(e)}")
+ return []
+
+ async def get_emoji_from_manager(self, hash) -> MaiEmoji:
+ """从EmojiManager中获取表情包
+
+ 参数:
+ hash:如果提供则只返回指定哈希值的表情包
+ """
+ for emoji in self.emoji_objects:
+ if emoji.hash == hash:
+ return emoji
+ return None
+
+ async def delete_emoji(self, emoji_hash: str) -> bool:
+ """根据哈希值删除表情包
+
+ Args:
+ emoji_hash: 表情包的哈希值
+
+ Returns:
+ bool: 是否成功删除
+ """
+ try:
+ self._ensure_db()
+
+ # 从emoji_objects中查找表情包对象
+ emoji = await self.get_emoji_from_manager(emoji_hash)
+
+ if not emoji:
+ logger.warning(f"[警告] 未找到哈希值为 {emoji_hash} 的表情包")
+ return False
+
+ # 使用MaiEmoji对象的delete方法删除表情包
+ success = await emoji.delete()
+
+ if success:
+ # 从emoji_objects列表中移除该对象
+ self.emoji_objects = [e for e in self.emoji_objects if e.hash != emoji_hash]
+ # 更新计数
+ self.emoji_num -= 1
+ logger.info(f"[统计] 当前表情包数量: {self.emoji_num}")
+
+ return True
+ else:
+ logger.error(f"[错误] 删除表情包失败: {emoji_hash}")
+ return False
+
+ except Exception as e:
+ logger.error(f"[错误] 删除表情包失败: {str(e)}")
+ logger.error(traceback.format_exc())
+ return False
+
+ def _emoji_objects_to_readable_list(self, emoji_objects):
+ """将表情包对象列表转换为可读的字符串列表
+
+ 参数:
+ emoji_objects: MaiEmoji对象列表
+
+ 返回:
+ list[str]: 可读的表情包信息字符串列表
+ """
+ emoji_info_list = []
+ for i, emoji in enumerate(emoji_objects):
+ # 转换时间戳为可读时间
+ time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(emoji.register_time))
+ # 构建每个表情包的信息字符串
+ emoji_info = (
+ f"编号: {i + 1}\n描述: {emoji.description}\n使用次数: {emoji.usage_count}\n添加时间: {time_str}\n"
+ )
+ emoji_info_list.append(emoji_info)
+ return emoji_info_list
+
+ async def replace_a_emoji(self, new_emoji: MaiEmoji):
+ """替换一个表情包
+
+ Args:
+ new_emoji: 新表情包对象
+
+ Returns:
+ bool: 是否成功替换表情包
+ """
+ try:
+ self._ensure_db()
+
+ # 获取所有表情包对象
+ all_emojis = self.emoji_objects
+
+ # 将表情包信息转换为可读的字符串
+ emoji_info_list = self._emoji_objects_to_readable_list(all_emojis)
+
+ # 构建提示词
+ prompt = (
+ f"{global_config.BOT_NICKNAME}的表情包存储已满({self.emoji_num}/{self.emoji_num_max}),"
+ f"需要决定是否删除一个旧表情包来为新表情包腾出空间。\n\n"
+ f"新表情包信息:\n"
+ f"描述: {new_emoji.description}\n\n"
+ f"现有表情包列表:\n" + "\n".join(emoji_info_list) + "\n\n"
+ "请决定:\n"
+ "1. 是否要删除某个现有表情包来为新表情包腾出空间?\n"
+ "2. 如果要删除,应该删除哪一个(给出编号)?\n"
+ "请只回答:'不删除'或'删除编号X'(X为表情包编号)。"
+ )
+
+ # 调用大模型进行决策
+ decision, _ = await self.llm_emotion_judge.generate_response_async(prompt, temperature=0.8)
+ logger.info(f"[决策] 大模型决策结果: {decision}")
+
+ # 解析决策结果
+ if "不删除" in decision:
+ logger.info("[决策] 决定不删除任何表情包")
+ return False
+
+ # 尝试从决策中提取表情包编号
+ match = re.search(r"删除编号(\d+)", decision)
+ if match:
+ emoji_index = int(match.group(1)) - 1 # 转换为0-based索引
+
+ # 检查索引是否有效
+ if 0 <= emoji_index < len(all_emojis):
+ emoji_to_delete = all_emojis[emoji_index]
+
+ # 删除选定的表情包
+ logger.info(f"[决策] 决定删除表情包: {emoji_to_delete.description}")
+ delete_success = await self.delete_emoji(emoji_to_delete.hash)
+
+ if delete_success:
+ # 修复:等待异步注册完成
+ register_success = await new_emoji.register_to_db()
+ if register_success:
+ self.emoji_objects.append(new_emoji)
+ self.emoji_num += 1
+ logger.success(f"[成功] 注册表情包: {new_emoji.description}")
+ return True
+ else:
+ logger.error(f"[错误] 注册表情包到数据库失败: {new_emoji.filename}")
+ return False
+ else:
+ logger.error("[错误] 删除表情包失败,无法完成替换")
+ return False
+ else:
+ logger.error(f"[错误] 无效的表情包编号: {emoji_index + 1}")
+ else:
+ logger.error(f"[错误] 无法从决策中提取表情包编号: {decision}")
+
+ return False
+
+ except Exception as e:
+ logger.error(f"[错误] 替换表情包失败: {str(e)}")
+ logger.error(traceback.format_exc())
+ return False
+
+ async def build_emoji_description(self, image_base64: str) -> Tuple[str, list]:
+ """获取表情包描述和情感列表
+
+ Args:
+ image_base64: 图片的base64编码
+
+ Returns:
+ Tuple[str, list]: 返回表情包描述和情感列表
+ """
+ try:
+ # 解码图片并获取格式
+ image_bytes = base64.b64decode(image_base64)
+ image_format = Image.open(io.BytesIO(image_bytes)).format.lower()
+
+ # 调用AI获取描述
+ if image_format == "gif" or image_format == "GIF":
+ image_base64 = image_manager.transform_gif(image_base64)
+ prompt = "这是一个动态图表情包,每一张图代表了动态图的某一帧,黑色背景代表透明,详细描述一下表情包表达的情感和内容,请关注其幽默和讽刺意味"
+ description, _ = await self.vlm.generate_response_for_image(prompt, image_base64, "jpg")
+ else:
+ prompt = "这是一个表情包,请详细描述一下表情包所表达的情感和内容,请关注其幽默和讽刺意味"
+ description, _ = await self.vlm.generate_response_for_image(prompt, image_base64, image_format)
+
+ # 审核表情包
+ if global_config.EMOJI_CHECK:
+ prompt = f'''
+ 这是一个表情包,请对这个表情包进行审核,标准如下:
+ 1. 必须符合"{global_config.EMOJI_CHECK_PROMPT}"的要求
+ 2. 不能是色情、暴力、等违法违规内容,必须符合公序良俗
+ 3. 不能是任何形式的截图,聊天记录或视频截图
+ 4. 不要出现5个以上文字
+ 请回答这个表情包是否满足上述要求,是则回答是,否则回答否,不要出现任何其他内容
+ '''
+ content, _ = await self.vlm.generate_response_for_image(prompt, image_base64, image_format)
+ if content == "否":
+ return None, []
+
+ # 分析情感含义
+ emotion_prompt = f"""
+ 基于这个表情包的描述:'{description}',请列出1-2个可能的情感标签,每个标签用一个词组表示,格式如下:
+ 幽默的讽刺
+ 悲伤的无奈
+ 愤怒的抗议
+ 愤怒的讽刺
+ 直接输出词组,词组检用逗号分隔。"""
+ 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()]
+
+ return f"[表情包:{description}]", emotions
+
+ except Exception as e:
+ logger.error(f"获取表情包描述失败: {str(e)}")
+ return "", []
+
+ async def register_emoji_by_filename(self, filename: str) -> bool:
+ """读取指定文件名的表情包图片,分析并注册到数据库
+
+ Args:
+ filename: 表情包文件名,必须位于EMOJI_DIR目录下
+
+ Returns:
+ bool: 注册是否成功
+ """
+ try:
+ # 使用MaiEmoji类创建表情包实例
+ new_emoji = MaiEmoji(filename, EMOJI_DIR)
+ await new_emoji.initialize_hash_format()
+ emoji_base64 = image_path_to_base64(os.path.join(EMOJI_DIR, filename))
+ description, emotions = await self.build_emoji_description(emoji_base64)
+ if description == "":
+ return False
+ new_emoji.description = description
+ new_emoji.emotion = emotions
+
+ # 检查是否已经注册过
+ # 对比内存中是否存在相同哈希值的表情包
+ if await self.get_emoji_from_manager(new_emoji.hash):
+ logger.warning(f"[警告] 表情包已存在: {filename}")
+ return False
+
+ if 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)
+ if not replaced:
+ logger.error("[错误] 替换表情包失败,无法完成注册")
+ return False
+ else:
+ # 修复:等待异步注册完成
+ register_success = await new_emoji.register_to_db()
+ if register_success:
+ self.emoji_objects.append(new_emoji)
+ self.emoji_num += 1
+ logger.success(f"[成功] 注册表情包: {filename}")
+ return True
+ else:
+ logger.error(f"[错误] 注册表情包到数据库失败: {filename}")
+ return False
+
+ except Exception as e:
+ logger.error(f"[错误] 注册表情包失败: {str(e)}")
+ logger.error(traceback.format_exc())
+ return False
+
+ async def clear_temp_emoji(self):
+ """每天清理临时表情包
+ 清理/data/emoji和/data/image目录下的所有文件
+ 当目录中文件数超过50时,会全部删除
+ """
+
+ logger.info("[清理] 开始清理临时表情包...")
+
+ # 清理emoji目录
+ emoji_dir = os.path.join(BASE_DIR, "emoji")
+ if os.path.exists(emoji_dir):
+ files = os.listdir(emoji_dir)
+ # 如果文件数超过50就全部删除
+ if len(files) > 50:
+ for filename in files:
+ file_path = os.path.join(emoji_dir, filename)
+ if os.path.isfile(file_path):
+ os.remove(file_path)
+ logger.debug(f"[清理] 删除表情包文件: {filename}")
+
+ # 清理image目录
+ image_dir = os.path.join(BASE_DIR, "image")
+ if os.path.exists(image_dir):
+ files = os.listdir(image_dir)
+ # 如果文件数超过50就全部删除
+ if len(files) > 50:
+ for filename in files:
+ file_path = os.path.join(image_dir, filename)
+ if os.path.isfile(file_path):
+ os.remove(file_path)
+ logger.debug(f"[清理] 删除图片文件: {filename}")
+
+ logger.success("[清理] 临时文件清理完成")
+
+
+# 创建全局单例
+emoji_manager = EmojiManager()
diff --git a/src/plugins/heartFC_chat/heartFC_Cycleinfo.py b/src/plugins/heartFC_chat/heartFC_Cycleinfo.py
new file mode 100644
index 00000000..96677384
--- /dev/null
+++ b/src/plugins/heartFC_chat/heartFC_Cycleinfo.py
@@ -0,0 +1,74 @@
+import time
+from typing import List, Optional, Dict, Any
+
+
+class CycleInfo:
+ """循环信息记录类"""
+
+ def __init__(self, cycle_id: int):
+ self.cycle_id = cycle_id
+ self.start_time = time.time()
+ self.end_time: Optional[float] = None
+ self.action_taken = False
+ self.action_type = "unknown"
+ self.reasoning = ""
+ self.timers: Dict[str, float] = {}
+ self.thinking_id = ""
+ self.replanned = False
+
+ # 添加响应信息相关字段
+ self.response_info: Dict[str, Any] = {
+ "response_text": [], # 回复的文本列表
+ "emoji_info": "", # 表情信息
+ "anchor_message_id": "", # 锚点消息ID
+ "reply_message_ids": [], # 回复消息ID列表
+ "sub_mind_thinking": "", # 子思维思考内容
+ }
+
+ def to_dict(self) -> Dict[str, Any]:
+ """将循环信息转换为字典格式"""
+ return {
+ "cycle_id": self.cycle_id,
+ "start_time": self.start_time,
+ "end_time": self.end_time,
+ "action_taken": self.action_taken,
+ "action_type": self.action_type,
+ "reasoning": self.reasoning,
+ "timers": self.timers,
+ "thinking_id": self.thinking_id,
+ "response_info": self.response_info,
+ }
+
+ def complete_cycle(self):
+ """完成循环,记录结束时间"""
+ self.end_time = time.time()
+
+ def set_action_info(self, action_type: str, reasoning: str, action_taken: bool):
+ """设置动作信息"""
+ self.action_type = action_type
+ self.reasoning = reasoning
+ self.action_taken = action_taken
+
+ def set_thinking_id(self, thinking_id: str):
+ """设置思考消息ID"""
+ self.thinking_id = thinking_id
+
+ def set_response_info(
+ self,
+ response_text: Optional[List[str]] = None,
+ emoji_info: Optional[str] = None,
+ anchor_message_id: Optional[str] = None,
+ reply_message_ids: Optional[List[str]] = None,
+ sub_mind_thinking: Optional[str] = None,
+ ):
+ """设置响应信息"""
+ if response_text is not None:
+ self.response_info["response_text"] = response_text
+ if emoji_info is not None:
+ self.response_info["emoji_info"] = emoji_info
+ if anchor_message_id is not None:
+ self.response_info["anchor_message_id"] = anchor_message_id
+ if reply_message_ids is not None:
+ self.response_info["reply_message_ids"] = reply_message_ids
+ if sub_mind_thinking is not None:
+ self.response_info["sub_mind_thinking"] = sub_mind_thinking
diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py
index ac8030f0..e9577e41 100644
--- a/src/plugins/heartFC_chat/heartFC_chat.py
+++ b/src/plugins/heartFC_chat/heartFC_chat.py
@@ -1,10 +1,11 @@
import asyncio
import time
import traceback
-from typing import List, Optional, Dict, Any, TYPE_CHECKING
-import json
+import random # <-- 添加导入
+from typing import List, Optional, Dict, Any, Deque
+from collections import deque
from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending
-from src.plugins.chat.message import MessageSet, Seg # Local import needed after move
+from src.plugins.chat.message import Seg # Local import needed after move
from src.plugins.chat.chat_stream import ChatStream
from src.plugins.chat.message import UserInfo
from src.plugins.chat.chat_stream import chat_manager
@@ -12,11 +13,18 @@ from src.common.logger import get_module_logger, LogConfig, PFC_STYLE_CONFIG #
from src.plugins.models.utils_model import LLMRequest
from src.config.config import global_config
from src.plugins.chat.utils_image import image_path_to_base64 # Local import needed after move
-from src.plugins.utils.timer_calculater import Timer # <--- Import Timer
+from src.plugins.utils.timer_calculator import Timer # <--- Import Timer
from src.plugins.heartFC_chat.heartFC_generator import HeartFCGenerator
from src.do_tool.tool_use import ToolUser
-from ..chat.message_sender import message_manager # <-- Import the global manager
-from src.plugins.chat.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_response # 导入新的JSON工具
+from src.heart_flow.sub_mind import SubMind
+from src.heart_flow.observation import Observation
+from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_manager
+import contextlib
+from src.plugins.utils.chat_message_builder import num_new_messages_since
+from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo
+from .heartFC_sender import HeartFCSender
# --- End import ---
@@ -31,38 +39,113 @@ interest_log_config = LogConfig(
logger = get_module_logger("HeartFCLoop", config=interest_log_config) # Logger Name Changed
-# Forward declaration for type hinting
-if TYPE_CHECKING:
- # Keep this if HeartFCController methods are still needed elsewhere,
- # but the instance variable will be removed from HeartFChatting
- # from .heartFC_controler import HeartFCController
- from src.heart_flow.heartflow import SubHeartflow, heartflow # <-- 同时导入 heartflow 实例用于类型检查
+# 默认动作定义
+DEFAULT_ACTIONS = {"no_reply": "不回复", "text_reply": "文本回复, 可选附带表情", "emoji_reply": "仅表情回复"}
-PLANNER_TOOL_DEFINITION = [
- {
- "type": "function",
- "function": {
- "name": "decide_reply_action",
- "description": "根据当前聊天内容和上下文,决定机器人是否应该回复以及如何回复。",
- "parameters": {
- "type": "object",
- "properties": {
- "action": {
- "type": "string",
- "enum": ["no_reply", "text_reply", "emoji_reply"],
- "description": "决定采取的行动:'no_reply'(不回复), 'text_reply'(文本回复, 可选附带表情) 或 'emoji_reply'(仅表情回复)。",
- },
- "reasoning": {"type": "string", "description": "做出此决定的简要理由。"},
- "emoji_query": {
- "type": "string",
- "description": "如果行动是'emoji_reply',指定表情的主题或概念。如果行动是'text_reply'且希望在文本后追加表情,也在此指定表情主题。",
+
+class ActionManager:
+ """动作管理器:控制每次决策可以使用的动作"""
+
+ def __init__(self):
+ # 初始化为默认动作集
+ self._available_actions: Dict[str, str] = DEFAULT_ACTIONS.copy()
+
+ def get_available_actions(self) -> Dict[str, str]:
+ """获取当前可用的动作集"""
+ return self._available_actions
+
+ def add_action(self, action_name: str, description: str) -> bool:
+ """
+ 添加新的动作
+
+ 参数:
+ action_name: 动作名称
+ description: 动作描述
+
+ 返回:
+ bool: 是否添加成功
+ """
+ if action_name in self._available_actions:
+ return False
+ self._available_actions[action_name] = description
+ return True
+
+ def remove_action(self, action_name: str) -> bool:
+ """
+ 移除指定动作
+
+ 参数:
+ action_name: 动作名称
+
+ 返回:
+ bool: 是否移除成功
+ """
+ if action_name not in self._available_actions:
+ return False
+ del self._available_actions[action_name]
+ return True
+
+ def clear_actions(self):
+ """清空所有动作"""
+ self._available_actions.clear()
+
+ def reset_to_default(self):
+ """重置为默认动作集"""
+ 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"],
},
},
- "required": ["action", "reasoning"],
- },
- },
- }
-]
+ }
+ ]
+
+
+# 在文件开头添加自定义异常类
+class HeartFCError(Exception):
+ """麦麦聊天系统基础异常类"""
+
+ pass
+
+
+class PlannerError(HeartFCError):
+ """规划器异常"""
+
+ pass
+
+
+class ReplierError(HeartFCError):
+ """回复器异常"""
+
+ pass
+
+
+class SenderError(HeartFCError):
+ """发送器异常"""
+
+ pass
class HeartFChatting:
@@ -72,7 +155,7 @@ class HeartFChatting:
其生命周期现在由其关联的 SubHeartflow 的 FOCUSED 状态控制。
"""
- def __init__(self, chat_id: str):
+ def __init__(self, chat_id: str, sub_mind: SubMind, observations: Observation):
"""
HeartFChatting 初始化函数
@@ -82,7 +165,14 @@ class HeartFChatting:
# 基础属性
self.stream_id: str = chat_id # 聊天流ID
self.chat_stream: Optional[ChatStream] = None # 关联的聊天流
- self.sub_hf: SubHeartflow = None # 关联的子心流
+ self.sub_mind: SubMind = sub_mind # 关联的子思维
+ self.observations: List[Observation] = observations # 关联的观察列表,用于监控聊天流状态
+
+ # 日志前缀
+ self.log_prefix: str = f"[{chat_manager.get_stream_name(chat_id) or chat_id}]"
+
+ # 动作管理器
+ self.action_manager = ActionManager()
# 初始化状态控制
self._initialized = False # 是否已初始化标志
@@ -91,11 +181,11 @@ class HeartFChatting:
# 依赖注入存储
self.gpt_instance = HeartFCGenerator() # 文本回复生成器
self.tool_user = ToolUser() # 工具使用实例
+ self.heart_fc_sender = HeartFCSender()
# LLM规划器配置
self.planner_llm = LLMRequest(
- model=global_config.llm_normal,
- temperature=global_config.llm_normal["temp"],
+ model=global_config.llm_plan,
max_tokens=1000,
request_type="action_planning", # 用于动作规划
)
@@ -104,494 +194,573 @@ class HeartFChatting:
self._loop_active: bool = False # 循环是否正在运行
self._loop_task: Optional[asyncio.Task] = None # 主循环任务
- def _get_log_prefix(self) -> str:
- """获取日志前缀,包含可读的流名称"""
- stream_name = chat_manager.get_stream_name(self.stream_id) or self.stream_id
- return f"[{stream_name}]"
+ # 添加循环信息管理相关的属性
+ self._cycle_counter = 0
+ self._cycle_history: Deque[CycleInfo] = deque(maxlen=10) # 保留最近10个循环的信息
+ self._current_cycle: Optional[CycleInfo] = None
async def _initialize(self) -> bool:
"""
- 懒初始化以使用提供的标识符解析chat_stream和sub_hf。
+ 懒初始化以使用提供的标识符解析chat_stream。
确保实例已准备好处理触发器。
"""
if self._initialized:
return True
- log_prefix = self._get_log_prefix() # 获取前缀
- try:
- self.chat_stream = chat_manager.get_stream(self.stream_id)
- if not self.chat_stream:
- logger.error(f"{log_prefix} 获取ChatStream失败。")
- return False
-
- # <-- 在这里导入 heartflow 实例
- from src.heart_flow.heartflow import heartflow
-
- self.sub_hf = heartflow.get_subheartflow(self.stream_id)
- if not self.sub_hf:
- logger.warning(f"{log_prefix} 获取SubHeartflow失败。一些功能可能受限。")
-
- self._initialized = True
- logger.info(f"麦麦感觉到了,激发了HeartFChatting{log_prefix} 初始化成功。")
- return True
- except Exception as e:
- logger.error(f"{log_prefix} 初始化失败: {e}")
- logger.error(traceback.format_exc())
+ self.chat_stream = chat_manager.get_stream(self.stream_id)
+ if not self.chat_stream:
+ logger.error(f"{self.log_prefix} 获取ChatStream失败。")
return False
+ # 更新日志前缀(以防流名称发生变化)
+ self.log_prefix = f"[{chat_manager.get_stream_name(self.stream_id) or self.stream_id}]"
+
+ self._initialized = True
+ logger.info(f"麦麦感觉到了,可以开始认真水群{self.log_prefix} ")
+ return True
+
async def start(self):
"""
- 显式尝试启动 HeartFChatting 的主循环。
- 如果循环未激活,则启动循环。
+ 启动 HeartFChatting 的主循环。
+ 注意:调用此方法前必须确保已经成功初始化。
"""
- log_prefix = self._get_log_prefix()
- if not self._initialized:
- if not await self._initialize():
- logger.error(f"{log_prefix} 无法启动循环: 初始化失败。")
- return
- logger.info(f"{log_prefix} 尝试显式启动循环...")
+ logger.info(f"{self.log_prefix} 开始认真水群(HFC)...")
await self._start_loop_if_needed()
async def _start_loop_if_needed(self):
"""检查是否需要启动主循环,如果未激活则启动。"""
- log_prefix = self._get_log_prefix()
- should_start_loop = False
- # 直接检查是否激活,无需检查计时器
- if not self._loop_active:
- should_start_loop = True
- self._loop_active = True # 标记为活动,防止重复启动
+ # 如果循环已经激活,直接返回
+ if self._loop_active:
+ return
- if should_start_loop:
- # 检查是否已有任务在运行(理论上不应该,因为 _loop_active=False)
- if self._loop_task and not self._loop_task.done():
- logger.warning(f"{log_prefix} 发现之前的循环任务仍在运行(不符合预期)。取消旧任务。")
- self._loop_task.cancel()
- try:
- # 等待旧任务确实被取消
- await asyncio.wait_for(self._loop_task, timeout=0.5)
- except (asyncio.CancelledError, asyncio.TimeoutError):
- pass # 忽略取消或超时错误
- self._loop_task = None # 清理旧任务引用
+ # 标记为活动状态,防止重复启动
+ self._loop_active = True
- logger.info(f"{log_prefix} 循环未激活,启动主循环...")
- # 创建新的循环任务
- self._loop_task = asyncio.create_task(self._run_pf_loop())
- # 添加完成回调
- self._loop_task.add_done_callback(self._handle_loop_completion)
- # else:
- # logger.trace(f"{log_prefix} 不需要启动循环(已激活)") # 可以取消注释以进行调试
+ # 检查是否已有任务在运行(理论上不应该,因为 _loop_active=False)
+ if self._loop_task and not self._loop_task.done():
+ logger.warning(f"{self.log_prefix} 发现之前的循环任务仍在运行(不符合预期)。取消旧任务。")
+ self._loop_task.cancel()
+ try:
+ # 等待旧任务确实被取消
+ await asyncio.wait_for(self._loop_task, timeout=0.5)
+ except (asyncio.CancelledError, asyncio.TimeoutError):
+ pass # 忽略取消或超时错误
+ self._loop_task = None # 清理旧任务引用
+
+ logger.info(f"{self.log_prefix} 启动认真水群(HFC)主循环...")
+ # 创建新的循环任务
+ self._loop_task = asyncio.create_task(self._hfc_loop())
+ # 添加完成回调
+ self._loop_task.add_done_callback(self._handle_loop_completion)
def _handle_loop_completion(self, task: asyncio.Task):
- """当 _run_pf_loop 任务完成时执行的回调。"""
- log_prefix = self._get_log_prefix()
+ """当 _hfc_loop 任务完成时执行的回调。"""
try:
exception = task.exception()
if exception:
- logger.error(f"{log_prefix} HeartFChatting: 麦麦脱离了聊天(异常): {exception}")
+ logger.error(f"{self.log_prefix} HeartFChatting: 麦麦脱离了聊天(异常): {exception}")
logger.error(traceback.format_exc()) # Log full traceback for exceptions
else:
# Loop completing normally now means it was cancelled/shutdown externally
- logger.info(f"{log_prefix} HeartFChatting: 麦麦脱离了聊天 (外部停止)")
+ logger.info(f"{self.log_prefix} HeartFChatting: 麦麦脱离了聊天 (外部停止)")
except asyncio.CancelledError:
- logger.info(f"{log_prefix} HeartFChatting: 麦麦脱离了聊天(任务取消)")
+ logger.info(f"{self.log_prefix} HeartFChatting: 麦麦脱离了聊天(任务取消)")
finally:
self._loop_active = False
self._loop_task = None
if self._processing_lock.locked():
- logger.warning(f"{log_prefix} HeartFChatting: 处理锁在循环结束时仍被锁定,强制释放。")
+ logger.warning(f"{self.log_prefix} HeartFChatting: 处理锁在循环结束时仍被锁定,强制释放。")
self._processing_lock.release()
- async def _run_pf_loop(self):
- """
- 主循环,持续进行计划并可能回复消息,直到被外部取消。
- 管理每个循环周期的处理锁。
- """
- log_prefix = self._get_log_prefix()
- logger.info(f"{log_prefix} HeartFChatting: 麦麦打算好好聊聊 (进入专注模式)")
+ async def _hfc_loop(self):
+ """主循环,持续进行计划并可能回复消息,直到被外部取消。"""
try:
- thinking_id = ""
- while True: # Loop indefinitely until cancelled
- cycle_timers = {} # <--- Initialize timers dict for this cycle
+ while True: # 主循环
+ # 创建新的循环信息
+ self._cycle_counter += 1
+ self._current_cycle = CycleInfo(self._cycle_counter)
- # Access MessageManager directly
- if message_manager.check_if_sending_message_exist(self.stream_id, thinking_id):
- # logger.info(f"{log_prefix} HeartFChatting: 麦麦还在发消息,等会再规划")
- await asyncio.sleep(1)
- continue
- else:
- # logger.info(f"{log_prefix} HeartFChatting: 麦麦不发消息了,开始规划")
- pass
-
- # 记录循环周期开始时间,用于计时和休眠计算
+ # 初始化周期状态
+ cycle_timers = {}
loop_cycle_start_time = time.monotonic()
- action_taken_this_cycle = False
- acquired_lock = False
- planner_start_db_time = 0.0 # 初始化
- try:
- with Timer("Total Cycle", cycle_timers) as _total_timer: # <--- Start total cycle timer
- # Use try_acquire pattern or timeout?
- await self._processing_lock.acquire()
- acquired_lock = True
- # logger.debug(f"{log_prefix} HeartFChatting: 循环获取到处理锁")
+ # 执行规划和处理阶段
+ async with self._get_cycle_context() as acquired_lock:
+ if not acquired_lock:
+ continue
- # 在规划前记录数据库时间戳
- planner_start_db_time = time.time()
+ # 记录规划开始时间点
+ planner_start_db_time = time.time()
- # --- Planner --- #
- planner_result = {}
- with Timer("Planner", cycle_timers): # <--- Start Planner timer
- planner_result = await self._planner()
- action = planner_result.get("action", "error")
- reasoning = planner_result.get("reasoning", "Planner did not provide reasoning.")
- emoji_query = planner_result.get("emoji_query", "")
- # current_mind = planner_result.get("current_mind", "[Mind unavailable]")
- # send_emoji_from_tools = planner_result.get("send_emoji_from_tools", "") # Emoji from tools
- observed_messages = planner_result.get("observed_messages", [])
- llm_error = planner_result.get("llm_error", False)
+ # 主循环:思考->决策->执行
+ action_taken, thinking_id = await self._think_plan_execute_loop(cycle_timers, planner_start_db_time)
- if llm_error:
- logger.error(f"{log_prefix} Planner LLM 失败,跳过本周期回复尝试。理由: {reasoning}")
- # Optionally add a longer sleep?
- action_taken_this_cycle = False # Ensure no action is counted
- # Continue to sleep logic
+ # 更新循环信息
+ self._current_cycle.set_thinking_id(thinking_id)
+ self._current_cycle.timers = cycle_timers
- elif action == "text_reply":
- logger.debug(f"{log_prefix} HeartFChatting: 麦麦决定回复文本. 理由: {reasoning}")
- action_taken_this_cycle = True
- anchor_message = await self._get_anchor_message(observed_messages)
- if not anchor_message:
- logger.error(f"{log_prefix} 循环: 无法获取锚点消息用于回复. 跳过周期.")
- else:
- # --- Create Thinking Message (Moved) ---
- thinking_id = await self._create_thinking_message(anchor_message)
- if not thinking_id:
- logger.error(f"{log_prefix} 循环: 无法创建思考ID. 跳过周期.")
- else:
- replier_result = None
- try:
- # --- Replier Work --- #
- with Timer("Replier", cycle_timers): # <--- Start Replier timer
- replier_result = await self._replier_work(
- anchor_message=anchor_message,
- thinking_id=thinking_id,
- reason=reasoning,
- )
- except Exception as e_replier:
- logger.error(f"{log_prefix} 循环: 回复器工作失败: {e_replier}")
- # self._cleanup_thinking_message(thinking_id) <-- Remove cleanup call
+ # 防止循环过快消耗资源
+ await self._handle_cycle_delay(action_taken, loop_cycle_start_time, self.log_prefix)
- if replier_result:
- # --- Sender Work --- #
- try:
- with Timer("Sender", cycle_timers): # <--- Start Sender timer
- await self._sender(
- thinking_id=thinking_id,
- anchor_message=anchor_message,
- response_set=replier_result,
- send_emoji=emoji_query,
- )
- # logger.info(f"{log_prefix} 循环: 发送器完成成功.")
- except Exception as e_sender:
- logger.error(f"{log_prefix} 循环: 发送器失败: {e_sender}")
- # _sender should handle cleanup, but double check
- # self._cleanup_thinking_message(thinking_id) <-- Remove cleanup call
- else:
- logger.warning(f"{log_prefix} 循环: 回复器未产生结果. 跳过发送.")
- # self._cleanup_thinking_message(thinking_id) <-- Remove cleanup call
- elif action == "emoji_reply":
- logger.info(
- f"{log_prefix} HeartFChatting: 麦麦决定回复表情 ('{emoji_query}'). 理由: {reasoning}"
- )
- action_taken_this_cycle = True
- anchor = await self._get_anchor_message(observed_messages)
- if anchor:
- try:
- # --- Handle Emoji (Moved) --- #
- with Timer("Emoji Handler", cycle_timers): # <--- Start Emoji timer
- await self._handle_emoji(anchor, [], emoji_query)
- except Exception as e_emoji:
- logger.error(f"{log_prefix} 循环: 发送表情失败: {e_emoji}")
- else:
- logger.warning(f"{log_prefix} 循环: 无法发送表情, 无法获取锚点.")
- action_taken_this_cycle = True # 即使发送失败,Planner 也决策了动作
+ # 完成当前循环并保存历史
+ self._current_cycle.complete_cycle()
+ self._cycle_history.append(self._current_cycle)
- elif action == "no_reply":
- logger.info(f"{log_prefix} HeartFChatting: 麦麦决定不回复. 原因: {reasoning}")
- action_taken_this_cycle = False # 标记为未执行动作
- # --- 新增:等待新消息 ---
- logger.debug(f"{log_prefix} HeartFChatting: 开始等待新消息 (自 {planner_start_db_time})...")
- observation = None
- if self.sub_hf:
- observation = self.sub_hf._get_primary_observation()
+ # 记录循环信息和计时器结果
+ timer_strings = []
+ for name, elapsed in cycle_timers.items():
+ formatted_time = f"{elapsed * 1000:.2f}毫秒" if elapsed < 1 else f"{elapsed:.2f}秒"
+ timer_strings.append(f"{name}: {formatted_time}")
- if observation:
- with Timer("Wait New Msg", cycle_timers): # <--- Start Wait timer
- wait_start_time = time.monotonic()
- while True:
- # Removed timer check within wait loop
- # async with self._timer_lock:
- # if self._loop_timer <= 0:
- # logger.info(f"{log_prefix} HeartFChatting: 等待新消息时计时器耗尽。")
- # break # 计时器耗尽,退出等待
-
- # 检查是否有新消息
- has_new = await observation.has_new_messages_since(planner_start_db_time)
- if has_new:
- logger.info(f"{log_prefix} HeartFChatting: 检测到新消息,结束等待。")
- break # 收到新消息,退出等待
-
- # 检查等待是否超时(例如,防止无限等待)
- if time.monotonic() - wait_start_time > 60: # 等待60秒示例
- logger.warning(f"{log_prefix} HeartFChatting: 等待新消息超时(60秒)。")
- break # 超时退出
-
- # 等待一段时间再检查
- try:
- await asyncio.sleep(1.5) # 检查间隔
- except asyncio.CancelledError:
- logger.info(f"{log_prefix} 等待新消息的 sleep 被中断。")
- raise # 重新抛出取消错误,以便外层循环处理
- else:
- logger.warning(
- f"{log_prefix} HeartFChatting: 无法获取 Observation 实例,无法等待新消息。"
- )
- # --- 等待结束 ---
-
- elif action == "error": # Action specifically set to error by planner
- logger.error(f"{log_prefix} HeartFChatting: Planner返回错误状态. 原因: {reasoning}")
- action_taken_this_cycle = False
-
- else: # Unknown action from planner
- logger.warning(
- f"{log_prefix} HeartFChatting: Planner返回未知动作 '{action}'. 原因: {reasoning}"
- )
- action_taken_this_cycle = False
-
- # --- Print Timer Results --- #
- if cycle_timers: # 先检查cycle_timers是否非空
- timer_strings = []
- for name, elapsed in cycle_timers.items():
- # 直接格式化存储在字典中的浮点数 elapsed
- formatted_time = f"{elapsed * 1000:.2f}毫秒" if elapsed < 1 else f"{elapsed:.2f}秒"
- timer_strings.append(f"{name}: {formatted_time}")
-
- if timer_strings: # 如果有有效计时器数据才打印
- logger.debug(f"{log_prefix} 该次决策耗时: {'; '.join(timer_strings)}")
-
- # --- Timer Decrement Removed --- #
- cycle_duration = time.monotonic() - loop_cycle_start_time
-
- except Exception as e_cycle:
- logger.error(f"{log_prefix} 循环周期执行时发生错误: {e_cycle}")
- logger.error(traceback.format_exc())
- if acquired_lock and self._processing_lock.locked():
- self._processing_lock.release()
- acquired_lock = False
- logger.warning(f"{log_prefix} 由于循环周期中的错误释放了处理锁.")
-
- finally:
- if acquired_lock:
- self._processing_lock.release()
- # logger.trace(f"{log_prefix} 循环释放了处理锁.") # Reduce noise
-
- # --- Timer Decrement Logging Removed ---
- # async with self._timer_lock:
- # self._loop_timer -= cycle_duration
- # # Log timer decrement less aggressively
- # if cycle_duration > 0.1 or not action_taken_this_cycle:
- # logger.debug(
- # f"{log_prefix} HeartFChatting: 周期耗时 {cycle_duration:.2f}s. 剩余时间: {self._loop_timer:.1f}s."
- # )
- if cycle_duration > 0.1:
- logger.debug(f"{log_prefix} HeartFChatting: 周期耗时 {cycle_duration:.2f}s.")
-
- # --- Delay --- #
- try:
- sleep_duration = 0.0
- if not action_taken_this_cycle and cycle_duration < 1.5:
- sleep_duration = 1.5 - cycle_duration
- elif cycle_duration < 0.2: # Keep minimal sleep even after action
- sleep_duration = 0.2
-
- if sleep_duration > 0:
- # logger.debug(f"{log_prefix} Sleeping for {sleep_duration:.2f}s")
- await asyncio.sleep(sleep_duration)
-
- except asyncio.CancelledError:
- logger.info(f"{log_prefix} Sleep interrupted, loop likely cancelling.")
- break # Exit loop immediately on cancellation
+ logger.debug(
+ f"{self.log_prefix} 第 #{self._current_cycle.cycle_id}次思考完成,"
+ f"耗时: {self._current_cycle.end_time - self._current_cycle.start_time:.2f}秒, "
+ f"动作: {self._current_cycle.action_type}"
+ + (f"\n计时器详情: {'; '.join(timer_strings)}" if timer_strings else "")
+ )
except asyncio.CancelledError:
- logger.info(f"{log_prefix} HeartFChatting: 麦麦的聊天主循环被取消了")
- except Exception as e_loop_outer:
- logger.error(f"{log_prefix} HeartFChatting: 麦麦的聊天主循环意外出错: {e_loop_outer}")
+ logger.info(f"{self.log_prefix} HeartFChatting: 麦麦的认真水群(HFC)被取消了")
+ except Exception as e:
+ logger.error(f"{self.log_prefix} HeartFChatting: 意外错误: {e}")
logger.error(traceback.format_exc())
- finally:
- # State reset is primarily handled by _handle_loop_completion callback
- logger.info(f"{log_prefix} HeartFChatting: 麦麦的聊天主循环结束。")
- async def _planner(self) -> Dict[str, Any]:
+ @contextlib.asynccontextmanager
+ async def _get_cycle_context(self):
+ """
+ 循环周期的上下文管理器
+
+ 用于确保资源的正确获取和释放:
+ 1. 获取处理锁
+ 2. 执行操作
+ 3. 释放锁
+ """
+ acquired = False
+ try:
+ await self._processing_lock.acquire()
+ acquired = True
+ yield acquired
+ finally:
+ if acquired and self._processing_lock.locked():
+ self._processing_lock.release()
+
+ async def _check_new_messages(self, start_time: float) -> bool:
+ """
+ 检查从指定时间点后是否有新消息
+
+ 参数:
+ start_time: 开始检查的时间点
+
+ 返回:
+ bool: 是否有新消息
+ """
+ try:
+ new_msg_count = num_new_messages_since(self.stream_id, start_time)
+ if new_msg_count > 0:
+ logger.info(f"{self.log_prefix} 检测到{new_msg_count}条新消息")
+ return True
+ return False
+ except Exception as e:
+ logger.error(f"{self.log_prefix} 检查新消息时出错: {e}")
+ return False
+
+ async def _think_plan_execute_loop(self, cycle_timers: dict, planner_start_db_time: float) -> tuple[bool, str]:
+ """执行规划阶段"""
+ try:
+ # think:思考
+ current_mind = await self._get_submind_thinking(cycle_timers)
+ # 记录子思维思考内容
+ if self._current_cycle:
+ self._current_cycle.set_response_info(sub_mind_thinking=current_mind)
+
+ # plan:决策
+ with Timer("决策", cycle_timers):
+ planner_result = await self._planner(current_mind, cycle_timers)
+
+ action = planner_result.get("action", "error")
+ reasoning = planner_result.get("reasoning", "未提供理由")
+
+ self._current_cycle.set_action_info(action, reasoning, False)
+
+ # 在获取规划结果后检查新消息
+ if await self._check_new_messages(planner_start_db_time):
+ if random.random() < 0.3:
+ logger.info(f"{self.log_prefix} 看到了新消息,麦麦决定重新观察和规划...")
+ # 重新规划
+ with Timer("重新决策", cycle_timers):
+ self._current_cycle.replanned = True
+ planner_result = await self._planner(current_mind, cycle_timers, is_re_planned=True)
+ logger.info(f"{self.log_prefix} 重新规划完成.")
+
+ # 解析规划结果
+ action = planner_result.get("action", "error")
+ reasoning = planner_result.get("reasoning", "未提供理由")
+ # 更新循环信息
+ self._current_cycle.set_action_info(action, reasoning, True)
+
+ # 处理LLM错误
+ if planner_result.get("llm_error"):
+ logger.error(f"{self.log_prefix} LLM失败: {reasoning}")
+ return False, ""
+
+ # execute:执行
+ with Timer("执行动作", cycle_timers):
+ return await self._handle_action(
+ action, reasoning, planner_result.get("emoji_query", ""), cycle_timers, planner_start_db_time
+ )
+
+ except PlannerError as e:
+ logger.error(f"{self.log_prefix} 规划错误: {e}")
+ # 更新循环信息
+ self._current_cycle.set_action_info("error", str(e), False)
+ return False, ""
+
+ async def _handle_action(
+ self, action: str, reasoning: str, emoji_query: str, cycle_timers: dict, planner_start_db_time: float
+ ) -> tuple[bool, str]:
+ """
+ 处理规划动作
+
+ 参数:
+ action: 动作类型
+ reasoning: 决策理由
+ emoji_query: 表情查询
+ cycle_timers: 计时器字典
+ planner_start_db_time: 规划开始时间
+
+ 返回:
+ tuple[bool, str]: (是否执行了动作, 思考消息ID)
+ """
+ action_handlers = {
+ "text_reply": self._handle_text_reply,
+ "emoji_reply": self._handle_emoji_reply,
+ "no_reply": self._handle_no_reply,
+ }
+
+ handler = action_handlers.get(action)
+ if not handler:
+ logger.warning(f"{self.log_prefix} 未知动作: {action}, 原因: {reasoning}")
+ return False, ""
+
+ try:
+ if action == "text_reply":
+ return await handler(reasoning, emoji_query, cycle_timers)
+ elif action == "emoji_reply":
+ return await handler(reasoning, emoji_query), ""
+ else: # no_reply
+ return await handler(reasoning, planner_start_db_time, cycle_timers), ""
+ except HeartFCError as e:
+ logger.error(f"{self.log_prefix} 处理{action}时出错: {e}")
+ return False, ""
+
+ async def _handle_text_reply(self, reasoning: str, emoji_query: str, cycle_timers: dict) -> tuple[bool, str]:
+ """
+ 处理文本回复
+
+ 工作流程:
+ 1. 获取锚点消息
+ 2. 创建思考消息
+ 3. 生成回复
+ 4. 发送消息
+
+ 参数:
+ reasoning: 回复原因
+ emoji_query: 表情查询
+ cycle_timers: 计时器字典
+
+ 返回:
+ tuple[bool, str]: (是否回复成功, 思考消息ID)
+ """
+
+ # 获取锚点消息
+ anchor_message = await self._get_anchor_message()
+ if not anchor_message:
+ raise PlannerError("无法获取锚点消息")
+
+ # 创建思考消息
+ thinking_id = await self._create_thinking_message(anchor_message)
+ if not thinking_id:
+ raise PlannerError("无法创建思考消息")
+
+ try:
+ # 生成回复
+ with Timer("生成回复", cycle_timers):
+ reply = await self._replier_work(
+ anchor_message=anchor_message,
+ thinking_id=thinking_id,
+ reason=reasoning,
+ )
+
+ if not reply:
+ raise ReplierError("回复生成失败")
+
+ # 发送消息
+
+ with Timer("发送消息", cycle_timers):
+ await self._sender(
+ thinking_id=thinking_id,
+ anchor_message=anchor_message,
+ response_set=reply,
+ send_emoji=emoji_query,
+ )
+
+ return True, thinking_id
+
+ except (ReplierError, SenderError) as e:
+ logger.error(f"{self.log_prefix} 回复失败: {e}")
+ return True, thinking_id # 仍然返回thinking_id以便跟踪
+
+ async def _handle_emoji_reply(self, reasoning: str, emoji_query: str) -> bool:
+ """
+ 处理表情回复
+
+ 工作流程:
+ 1. 获取锚点消息
+ 2. 发送表情
+
+ 参数:
+ reasoning: 回复原因
+ emoji_query: 表情查询
+
+ 返回:
+ bool: 是否发送成功
+ """
+ logger.info(f"{self.log_prefix} 决定回复表情({emoji_query}): {reasoning}")
+
+ try:
+ anchor = await self._get_anchor_message()
+ if not anchor:
+ raise PlannerError("无法获取锚点消息")
+
+ await self._handle_emoji(anchor, [], emoji_query)
+ return True
+
+ except Exception as e:
+ logger.error(f"{self.log_prefix} 表情发送失败: {e}")
+ return False
+
+ async def _handle_no_reply(self, reasoning: str, planner_start_db_time: float, cycle_timers: dict) -> bool:
+ """
+ 处理不回复的情况
+
+ 工作流程:
+ 1. 等待新消息
+ 2. 超时或收到新消息时返回
+
+ 参数:
+ reasoning: 不回复的原因
+ planner_start_db_time: 规划开始时间
+ cycle_timers: 计时器字典
+
+ 返回:
+ bool: 是否成功处理
+ """
+ logger.info(f"{self.log_prefix} 决定不回复: {reasoning}")
+
+ observation = self.observations[0] if self.observations else None
+
+ try:
+ with Timer("Wait New Msg", cycle_timers):
+ return await self._wait_for_new_message(observation, planner_start_db_time, self.log_prefix)
+ except asyncio.CancelledError:
+ logger.info(f"{self.log_prefix} 等待被中断")
+ raise
+
+ async def _wait_for_new_message(self, observation, planner_start_db_time: float, log_prefix: str) -> bool:
+ """
+ 等待新消息
+
+ 参数:
+ observation: 观察实例
+ planner_start_db_time: 开始等待的时间
+ log_prefix: 日志前缀
+
+ 返回:
+ bool: 是否检测到新消息
+ """
+ wait_start_time = time.monotonic()
+ while True:
+ if await observation.has_new_messages_since(planner_start_db_time):
+ logger.info(f"{log_prefix} 检测到新消息")
+ return True
+
+ if time.monotonic() - wait_start_time > 300:
+ logger.warning(f"{log_prefix} 等待超时(300秒)")
+ return False
+
+ await asyncio.sleep(1.5)
+
+ async def _log_cycle_timers(self, cycle_timers: dict, log_prefix: str):
+ """记录循环周期的计时器结果"""
+ if cycle_timers:
+ timer_strings = []
+ for name, elapsed in cycle_timers.items():
+ formatted_time = f"{elapsed * 1000:.2f}毫秒" if elapsed < 1 else f"{elapsed:.2f}秒"
+ timer_strings.append(f"{name}: {formatted_time}")
+
+ if timer_strings:
+ logger.debug(f"{log_prefix} 该次决策耗时: {'; '.join(timer_strings)}")
+
+ async def _handle_cycle_delay(self, action_taken_this_cycle: bool, cycle_start_time: float, log_prefix: str):
+ """处理循环延迟"""
+ cycle_duration = time.monotonic() - cycle_start_time
+ # if cycle_duration > 0.1:
+ # logger.debug(f"{log_prefix} HeartFChatting: 周期耗时 {cycle_duration:.2f}s.")
+
+ try:
+ sleep_duration = 0.0
+ if not action_taken_this_cycle and cycle_duration < 1:
+ sleep_duration = 1 - cycle_duration
+ elif cycle_duration < 0.2:
+ sleep_duration = 0.2
+
+ if sleep_duration > 0:
+ await asyncio.sleep(sleep_duration)
+
+ except asyncio.CancelledError:
+ logger.info(f"{log_prefix} Sleep interrupted, loop likely cancelling.")
+ raise
+
+ async def _get_submind_thinking(self, cycle_timers: dict) -> str:
+ """
+ 获取子思维的思考结果
+
+ 返回:
+ str: 思考结果,如果思考失败则返回错误信息
+ """
+ try:
+ with Timer("观察", cycle_timers):
+ observation = self.observations[0]
+ await observation.observe()
+
+ # 获取上一个循环的信息
+ last_cycle = self._cycle_history[-1] if self._cycle_history else None
+
+ with Timer("思考", cycle_timers):
+ # 获取上一个循环的动作
+ # 传递上一个循环的信息给 do_thinking_before_reply
+ current_mind, _past_mind = await self.sub_mind.do_thinking_before_reply(last_cycle=last_cycle)
+ return current_mind
+ except Exception as e:
+ logger.error(f"{self.log_prefix}[SubMind] 思考失败: {e}")
+ logger.error(traceback.format_exc())
+ return "[思考时出错]"
+
+ async def _planner(self, current_mind: str, cycle_timers: dict, is_re_planned: bool = False) -> Dict[str, Any]:
"""
规划器 (Planner): 使用LLM根据上下文决定是否和如何回复。
+
+ 参数:
+ current_mind: 子思维的当前思考结果
"""
- log_prefix = self._get_log_prefix()
- observed_messages: List[dict] = []
- tool_result_info = {}
- get_mid_memory_id = []
- # send_emoji_from_tools = "" # Emoji suggested by tools
- current_mind: Optional[str] = None
- llm_error = False # Flag for LLM failure
+ logger.info(f"{self.log_prefix}[Planner] 开始{'重新' if is_re_planned else ''}执行规划器")
- # --- Ensure SubHeartflow is available ---
- if not self.sub_hf:
- # Attempt to re-fetch if missing (might happen if initialization order changes)
- self.sub_hf = heartflow.get_subheartflow(self.stream_id)
- if not self.sub_hf:
- logger.error(f"{log_prefix}[Planner] SubHeartflow is not available. Cannot proceed.")
- return {
- "action": "error",
- "reasoning": "SubHeartflow unavailable",
- "llm_error": True,
- "observed_messages": [],
- }
-
- try:
- # Access observation via self.sub_hf
- observation = self.sub_hf._get_primary_observation()
+ # 获取观察信息
+ observation = self.observations[0]
+ if is_re_planned:
await observation.observe()
- observed_messages = observation.talking_message
- observed_messages_str = observation.talking_message_str
- except Exception as e:
- logger.error(f"{log_prefix}[Planner] 获取观察信息时出错: {e}")
- # Handle error gracefully, maybe return an error state
- observed_messages_str = "[Error getting observation]"
- # Consider returning error here if observation is critical
- # --- 结束获取观察信息 --- #
-
- # --- (Moved from _replier_work) 1. 思考前使用工具 --- #
- try:
- # Access tool_user directly
- tool_result = await self.tool_user.use_tool(
- message_txt=observed_messages_str,
- chat_stream=self.chat_stream,
- observation=self.sub_hf._get_primary_observation(),
- )
- if tool_result.get("used_tools", False):
- tool_result_info = tool_result.get("structured_info", {})
- logger.debug(f"{log_prefix}[Planner] 规划前工具结果: {tool_result_info}")
-
- get_mid_memory_id = [
- mem["content"] for mem in tool_result_info.get("mid_chat_mem", []) if "content" in mem
- ]
-
- except Exception as e_tool:
- logger.error(f"{log_prefix}[Planner] 规划前工具使用失败: {e_tool}")
- # --- 结束工具使用 --- #
-
- # --- (Moved from _replier_work) 2. SubHeartflow 思考 --- #
- try:
- current_mind, _past_mind = await self.sub_hf.do_thinking_before_reply(
- extra_info=tool_result_info,
- obs_id=get_mid_memory_id,
- )
- # logger.debug(f"{log_prefix}[Planner] SubHF Mind: {current_mind}")
- except Exception as e_subhf:
- logger.error(f"{log_prefix}[Planner] SubHeartflow 思考失败: {e_subhf}")
- current_mind = "[思考时出错]"
- # --- 结束 SubHeartflow 思考 --- #
+ observed_messages = observation.talking_message
+ observed_messages_str = observation.talking_message_str
# --- 使用 LLM 进行决策 --- #
- action = "no_reply" # Default action
- emoji_query = "" # Default emoji query (used if action is emoji_reply or text_reply with emoji)
+ action = "no_reply" # 默认动作
+ emoji_query = "" # 默认表情查询
reasoning = "默认决策或获取决策失败"
+ llm_error = False # LLM错误标志
try:
- prompt = await self._build_planner_prompt(observed_messages_str, current_mind)
+ # 构建提示词
+
+ if is_re_planned:
+ replan_prompt = await self._build_replan_prompt(
+ self._current_cycle.action_type, self._current_cycle.reasoning
+ )
+ prompt = replan_prompt
+ else:
+ replan_prompt = ""
+ prompt = await self._build_planner_prompt(
+ observed_messages_str, current_mind, self.sub_mind.structured_info, replan_prompt
+ )
payload = {
- "model": self.planner_llm.model_name,
+ "model": global_config.llm_plan["name"],
"messages": [{"role": "user", "content": prompt}],
- "tools": PLANNER_TOOL_DEFINITION,
+ "tools": self.action_manager.get_planner_tool_definition(),
"tool_choice": {"type": "function", "function": {"name": "decide_reply_action"}},
}
- response = await self.planner_llm._execute_request(
- endpoint="/chat/completions", payload=payload, prompt=prompt
- )
+ # 执行LLM请求
- if len(response) == 3:
- _, _, tool_calls = response
- if tool_calls and isinstance(tool_calls, list) and len(tool_calls) > 0:
- tool_call = tool_calls[0]
- if (
- tool_call.get("type") == "function"
- and tool_call.get("function", {}).get("name") == "decide_reply_action"
- ):
- try:
- arguments = json.loads(tool_call["function"]["arguments"])
- action = arguments.get("action", "no_reply")
- reasoning = arguments.get("reasoning", "未提供理由")
- # Planner explicitly provides emoji query if action is emoji_reply or text_reply wants emoji
- emoji_query = arguments.get("emoji_query", "")
- logger.debug(
- f"{log_prefix}[Planner] LLM Prompt: {prompt}\n决策: {action}, 理由: {reasoning}, EmojiQuery: '{emoji_query}'"
- )
- except json.JSONDecodeError as json_e:
- logger.error(
- f"{log_prefix}[Planner] 解析工具参数失败: {json_e}. Args: {tool_call['function'].get('arguments')}"
- )
- action = "error"
- reasoning = "工具参数解析失败"
- llm_error = True
- except Exception as parse_e:
- logger.error(f"{log_prefix}[Planner] 处理工具参数时出错: {parse_e}")
- action = "error"
- reasoning = "处理工具参数时出错"
- llm_error = True
- else:
+ try:
+ response = await self.planner_llm._execute_request(
+ endpoint="/chat/completions", payload=payload, prompt=prompt
+ )
+ except Exception as req_e:
+ logger.error(f"{self.log_prefix}[Planner] LLM请求执行失败: {req_e}")
+ return {
+ "action": "error",
+ "reasoning": f"LLM请求执行失败: {req_e}",
+ "emoji_query": "",
+ "current_mind": current_mind,
+ "observed_messages": observed_messages,
+ "llm_error": True,
+ }
+
+ # 处理LLM响应
+ with Timer("使用工具", cycle_timers):
+ # 使用辅助函数处理工具调用响应
+ success, arguments, error_msg = process_llm_tool_response(
+ response, expected_tool_name="decide_reply_action", log_prefix=f"{self.log_prefix}[Planner] "
+ )
+
+ if success:
+ # 提取决策参数
+ action = arguments.get("action", "no_reply")
+ # 验证动作是否在可用动作集中
+ if action not in self.action_manager.get_available_actions():
logger.warning(
- f"{log_prefix}[Planner] LLM 未按预期调用 'decide_reply_action' 工具。Tool calls: {tool_calls}"
+ f"{self.log_prefix}[Planner] LLM返回了未授权的动作: {action},使用默认动作no_reply"
)
- action = "error"
- reasoning = "LLM未调用预期工具"
- llm_error = True
+ action = "no_reply"
+ reasoning = f"LLM返回了未授权的动作: {action}"
+ else:
+ reasoning = arguments.get("reasoning", "未提供理由")
+ emoji_query = arguments.get("emoji_query", "")
+
+ # 记录决策结果
+ logger.debug(
+ f"{self.log_prefix}[要做什么]\nPrompt:\n{prompt}\n\n决策结果: {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'"
+ )
else:
- logger.warning(f"{log_prefix}[Planner] LLM 响应中未包含有效的工具调用。Tool calls: {tool_calls}")
+ # 处理工具调用失败
+ logger.warning(f"{self.log_prefix}[Planner] {error_msg}")
action = "error"
- reasoning = "LLM响应无工具调用"
+ reasoning = error_msg
llm_error = True
- else:
- logger.warning(f"{log_prefix}[Planner] LLM 未返回预期的工具调用响应。Response parts: {len(response)}")
- action = "error"
- reasoning = "LLM响应格式错误"
- llm_error = True
except Exception as llm_e:
- logger.error(f"{log_prefix}[Planner] Planner LLM 调用失败: {llm_e}")
- # logger.error(traceback.format_exc()) # Maybe too verbose for loop?
+ logger.error(f"{self.log_prefix}[Planner] Planner LLM处理过程中出错: {llm_e}")
+ logger.error(traceback.format_exc()) # 记录完整堆栈以便调试
action = "error"
- reasoning = f"LLM 调用失败: {llm_e}"
+ reasoning = f"LLM处理失败: {llm_e}"
llm_error = True
# --- 结束 LLM 决策 --- #
return {
"action": action,
"reasoning": reasoning,
- "emoji_query": emoji_query, # Explicit query from Planner/LLM
+ "emoji_query": emoji_query,
"current_mind": current_mind,
- # "send_emoji_from_tools": send_emoji_from_tools, # Emoji suggested by tools (used as fallback)
"observed_messages": observed_messages,
"llm_error": llm_error,
}
- async def _get_anchor_message(self, observed_messages: List[dict]) -> Optional[MessageRecv]:
+ async def _get_anchor_message(self) -> Optional[MessageRecv]:
"""
重构观察到的最后一条消息作为回复的锚点,
如果重构失败或观察为空,则创建一个占位符。
"""
try:
- # --- Create Placeholder --- #
placeholder_id = f"mid_pf_{int(time.time() * 1000)}"
placeholder_user = UserInfo(
user_id="system_trigger", user_nickname="System Trigger", platform=self.chat_stream.platform
@@ -612,12 +781,12 @@ class HeartFChatting:
anchor_message = MessageRecv(placeholder_msg_dict)
anchor_message.update_chat_stream(self.chat_stream)
logger.info(
- f"{self._get_log_prefix()} Created placeholder anchor message: 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
except Exception as e:
- logger.error(f"{self._get_log_prefix()} Error getting/creating anchor message: {e}")
+ logger.error(f"{self.log_prefix} Error getting/creating anchor message: {e}")
logger.error(traceback.format_exc())
return None
@@ -630,84 +799,113 @@ class HeartFChatting:
send_emoji: str, # Emoji query decided by planner or tools
):
"""
- 发送器 (Sender): 使用本类的方法发送生成的回复。
+ 发送器 (Sender): 使用 HeartFCSender 实例发送生成的回复。
处理相关的操作,如发送表情和更新关系。
"""
- log_prefix = self._get_log_prefix()
+ logger.info(f"{self.log_prefix}开始发送回复 (使用 HeartFCSender)")
first_bot_msg: Optional[MessageSending] = None
- # 尝试发送回复消息
- first_bot_msg = await self._send_response_messages(anchor_message, response_set, thinking_id)
- if first_bot_msg:
- # --- 处理关联表情(如果指定) --- #
- if send_emoji:
- logger.info(f"{log_prefix}[Sender-{thinking_id}] 正在发送关联表情: '{send_emoji}'")
- # 优先使用first_bot_msg作为锚点,否则回退到原始锚点
- emoji_anchor = first_bot_msg if first_bot_msg else anchor_message
- await self._handle_emoji(emoji_anchor, response_set, send_emoji)
+ try:
+ # _send_response_messages 现在将使用 self.sender 内部处理注册和发送
+ # 它需要负责创建 MessageThinking 和 MessageSending 对象
+ # 并调用 self.sender.register_thinking 和 self.sender.type_and_send_message
+ first_bot_msg = await self._send_response_messages(
+ anchor_message=anchor_message, response_set=response_set, thinking_id=thinking_id
+ )
- else:
- # logger.warning(f"{log_prefix}[Sender-{thinking_id}] 发送回复失败(_send_response_messages返回None)。思考消息{thinking_id}可能已被移除。")
- # 无需清理,因为_send_response_messages返回None意味着已处理/已删除
- raise RuntimeError("发送回复失败,_send_response_messages返回None")
+ if first_bot_msg:
+ # --- 处理关联表情(如果指定) --- #
+ if send_emoji:
+ logger.info(f"{self.log_prefix}正在发送关联表情: '{send_emoji}'")
+ # 优先使用 first_bot_msg 作为锚点,否则回退到原始锚点
+ emoji_anchor = first_bot_msg
+ await self._handle_emoji(emoji_anchor, response_set, send_emoji)
+ else:
+ # 如果 _send_response_messages 返回 None,表示在发送前就失败或没有消息可发送
+ logger.warning(
+ f"{self.log_prefix}[Sender-{thinking_id}] 未能发送任何回复消息 (_send_response_messages 返回 None)。"
+ )
+ # 这里可能不需要抛出异常,取决于 _send_response_messages 的具体实现
+
+ except Exception as e:
+ # 异常现在由 type_and_send_message 内部处理日志,这里只记录发送流程失败
+ logger.error(f"{self.log_prefix}[Sender-{thinking_id}] 发送回复过程中遇到错误: {e}")
+ # 思考状态应已在 type_and_send_message 的 finally 块中清理
+ # 可以选择重新抛出或根据业务逻辑处理
+ # raise RuntimeError(f"发送回复失败: {e}") from e
async def shutdown(self):
- """
- Gracefully shuts down the HeartFChatting instance by cancelling the active loop task.
- """
- log_prefix = self._get_log_prefix()
- logger.info(f"{log_prefix} Shutting down HeartFChatting...")
+ """优雅关闭HeartFChatting实例,取消活动循环任务"""
+ logger.info(f"{self.log_prefix} 正在关闭HeartFChatting...")
+
+ # 取消循环任务
if self._loop_task and not self._loop_task.done():
- logger.info(f"{log_prefix} Cancelling active PF loop task.")
+ logger.info(f"{self.log_prefix} 正在取消HeartFChatting循环任务")
self._loop_task.cancel()
try:
- await asyncio.wait_for(self._loop_task, timeout=1.0) # Shorter timeout?
- except asyncio.CancelledError:
- logger.info(f"{log_prefix} PF loop task cancelled successfully.")
- except asyncio.TimeoutError:
- logger.warning(f"{log_prefix} Timeout waiting for PF loop task cancellation.")
+ await asyncio.wait_for(self._loop_task, timeout=1.0)
+ logger.info(f"{self.log_prefix} HeartFChatting循环任务已取消")
+ except (asyncio.CancelledError, asyncio.TimeoutError):
+ pass
except Exception as e:
- logger.error(f"{log_prefix} Error during loop task cancellation: {e}")
+ logger.error(f"{self.log_prefix} 取消循环任务出错: {e}")
else:
- logger.info(f"{log_prefix} No active PF loop task found to cancel.")
+ logger.info(f"{self.log_prefix} 没有活动的HeartFChatting循环任务")
+ # 清理状态
self._loop_active = False
self._loop_task = None
if self._processing_lock.locked():
- logger.warning(f"{log_prefix} Releasing processing lock during shutdown.")
self._processing_lock.release()
- logger.info(f"{log_prefix} HeartFChatting shutdown complete.")
+ logger.warning(f"{self.log_prefix} 已释放处理锁")
- async def _build_planner_prompt(self, observed_messages_str: str, current_mind: Optional[str]) -> str:
+ logger.info(f"{self.log_prefix} HeartFChatting关闭完成")
+
+ async def _build_replan_prompt(self, action: str, reasoning: str) -> str:
+ """构建 Replanner LLM 的提示词"""
+ prompt = (await global_prompt_manager.get_prompt_async("replan_prompt")).format(
+ action=action,
+ reasoning=reasoning,
+ )
+ return prompt
+
+ async def _build_planner_prompt(
+ self,
+ observed_messages_str: str,
+ current_mind: Optional[str],
+ structured_info: Dict[str, Any],
+ replan_prompt: str,
+ ) -> str:
"""构建 Planner LLM 的提示词"""
- prompt = f"你的名字是 {global_config.BOT_NICKNAME}。是一个女大二学生,正在QQ聊天,正在决定是否以及如何回应当前的聊天。\n"
+ # 准备结构化信息块
+ structured_info_block = ""
+ if structured_info:
+ structured_info_block = f"以下是一些额外的信息:\n{structured_info}\n"
+ # 准备聊天内容块
+ chat_content_block = ""
if observed_messages_str:
- prompt += "观察到的最新聊天内容如下 (最近的消息在最后):\n---\n"
- prompt += observed_messages_str
- prompt += "\n---"
+ chat_content_block = "观察到的最新聊天内容如下:\n---\n"
+ chat_content_block += observed_messages_str
+ chat_content_block += "\n---"
else:
- prompt += "当前没有观察到新的聊天内容。\n"
+ chat_content_block = "当前没有观察到新的聊天内容。\n"
- prompt += "\n看了以上内容,你产生的内心想法是:"
+ # 准备当前思维块
+ current_mind_block = ""
if current_mind:
- prompt += f"\n---\n{current_mind}\n---\n\n"
+ current_mind_block = f"{current_mind}"
else:
- prompt += " [没有特别的想法] \n\n"
+ current_mind_block = "[没有特别的想法]"
- prompt += (
- "请结合你的内心想法和观察到的聊天内容,分析情况并使用 'decide_reply_action' 工具来决定你的最终行动。\n"
- "决策依据:\n"
- "1. 如果聊天内容无聊、与你无关、或者你的内心想法认为不适合回复(例如在讨论你不懂或不感兴趣的话题),选择 'no_reply'。\n"
- "2. 如果聊天内容值得回应,且适合用文字表达(参考你的内心想法),选择 'text_reply'。如果你有情绪想表达,想在文字后追加一个表达情绪的表情,请同时提供 'emoji_query' (例如:'开心的'、'惊讶的')。\n"
- "3. 如果聊天内容或你的内心想法适合用一个表情来回应(例如表示赞同、惊讶、无语等),选择 'emoji_reply' 并提供表情主题 'emoji_query'。\n"
- "4. 如果最后一条消息是你自己发的,并且之后没有人回复你,通常选择 'no_reply',除非有特殊原因需要追问。\n"
- "5. 除非大家都在这么做,或者有特殊理由,否则不要重复别人刚刚说过的话或简单附和。\n"
- "6. 表情包是用来表达情绪的,不要直接回复或评价别人的表情包,而是根据对话内容和情绪选择是否用表情回应。\n"
- "7. 如果观察到的内容只有你自己的发言,选择 'no_reply'。\n"
- "8. 不要回复你自己的话,不要把自己的话当做别人说的。\n"
- "必须调用 'decide_reply_action' 工具并提供 'action' 和 'reasoning'。如果选择了 'emoji_reply' 或者选择了 'text_reply' 并想追加表情,则必须提供 'emoji_query'。"
+ # 获取提示词模板并填充数据
+ prompt = (await global_prompt_manager.get_prompt_async("planner_prompt")).format(
+ bot_name=global_config.BOT_NICKNAME,
+ structured_info_block=structured_info_block,
+ chat_content_block=chat_content_block,
+ current_mind_block=current_mind_block,
+ replan=replan_prompt,
)
return prompt
@@ -722,26 +920,24 @@ class HeartFChatting:
"""
回复器 (Replier): 核心逻辑用于生成回复。
"""
- log_prefix = self._get_log_prefix()
response_set: Optional[List[str]] = None
try:
response_set = await self.gpt_instance.generate_response(
- current_mind_info=self.sub_hf.current_mind,
+ structured_info=self.sub_mind.structured_info,
+ current_mind_info=self.sub_mind.current_mind,
reason=reason,
message=anchor_message, # Pass anchor_message positionally (matches 'message' parameter)
thinking_id=thinking_id, # Pass thinking_id positionally
)
if not response_set:
- logger.warning(f"{log_prefix}[Replier-{thinking_id}] LLM生成了一个空回复集。")
+ logger.warning(f"{self.log_prefix}[Replier-{thinking_id}] LLM生成了一个空回复集。")
return None
- # --- 准备并返回结果 --- #
- # logger.info(f"{log_prefix}[Replier-{thinking_id}] 成功生成了回复集: {' '.join(response_set)[:50]}...")
return response_set
except Exception as e:
- logger.error(f"{log_prefix}[Replier-{thinking_id}] Unexpected error in replier_work: {e}")
+ logger.error(f"{self.log_prefix}[Replier-{thinking_id}] Unexpected error in replier_work: {e}")
logger.error(traceback.format_exc())
return None
@@ -749,7 +945,7 @@ class HeartFChatting:
async def _create_thinking_message(self, anchor_message: Optional[MessageRecv]) -> Optional[str]:
"""创建思考消息 (尝试锚定到 anchor_message)"""
if not anchor_message or not anchor_message.chat_stream:
- logger.error(f"{self._get_log_prefix()} 无法创建思考消息,缺少有效的锚点消息或聊天流。")
+ logger.error(f"{self.log_prefix} 无法创建思考消息,缺少有效的锚点消息或聊天流。")
return None
chat = anchor_message.chat_stream
@@ -770,85 +966,102 @@ class HeartFChatting:
thinking_start_time=thinking_time_point,
)
# Access MessageManager directly
- await message_manager.add_message(thinking_message)
+ await self.heart_fc_sender.register_thinking(thinking_message)
return thinking_id
async def _send_response_messages(
self, anchor_message: Optional[MessageRecv], response_set: List[str], thinking_id: str
) -> Optional[MessageSending]:
- """发送回复消息 (尝试锚定到 anchor_message)"""
+ """发送回复消息 (尝试锚定到 anchor_message),使用 HeartFCSender"""
if not anchor_message or not anchor_message.chat_stream:
- logger.error(f"{self._get_log_prefix()} 无法发送回复,缺少有效的锚点消息或聊天流。")
+ logger.error(f"{self.log_prefix} 无法发送回复,缺少有效的锚点消息或聊天流。")
return None
chat = anchor_message.chat_stream
- # Access MessageManager directly
- container = await message_manager.get_container(chat.stream_id)
- thinking_message = None
+ chat_id = chat.stream_id
+ stream_name = chat_manager.get_stream_name(chat_id) or chat_id # 获取流名称用于日志
- # 移除思考消息
- for msg in container.messages[:]: # Iterate over a copy
- if isinstance(msg, MessageThinking) and msg.message_info.message_id == thinking_id:
- thinking_message = msg
- container.messages.remove(msg) # Remove the message directly here
- logger.debug(f"{self._get_log_prefix()} Removed thinking message {thinking_id} via iteration.")
- break
+ # 检查思考过程是否仍在进行,并获取开始时间
+ thinking_start_time = await self.heart_fc_sender.get_thinking_start_time(chat_id, thinking_id)
- if not thinking_message:
- stream_name = chat_manager.get_stream_name(chat.stream_id) or chat.stream_id # 获取流名称
- logger.warning(f"[{stream_name}] {thinking_id},思考太久了,超时被移除")
+ if thinking_start_time is None:
+ logger.warning(f"[{stream_name}] {thinking_id} 思考过程未找到或已结束,无法发送回复。")
return None
- thinking_start_time = thinking_message.thinking_start_time
- message_set = MessageSet(chat, thinking_id)
+ # 记录锚点消息ID和回复文本(在发送前记录)
+ self._current_cycle.set_response_info(
+ response_text=response_set, anchor_message_id=anchor_message.message_info.message_id
+ )
+
mark_head = False
- first_bot_msg = None
+ first_bot_msg: Optional[MessageSending] = None
+ reply_message_ids = [] # 记录实际发送的消息ID
bot_user_info = UserInfo(
user_id=global_config.BOT_QQ,
user_nickname=global_config.BOT_NICKNAME,
platform=anchor_message.message_info.platform,
)
- for msg_text in response_set:
+
+ for i, msg_text in enumerate(response_set):
+ # 为每个消息片段生成唯一ID
+ part_message_id = f"{thinking_id}_{i}"
message_segment = Seg(type="text", data=msg_text)
bot_message = MessageSending(
- message_id=thinking_id, # 使用 thinking_id 作为批次标识
+ message_id=part_message_id, # 使用片段的唯一ID
chat_stream=chat,
bot_user_info=bot_user_info,
- sender_info=anchor_message.message_info.user_info, # 发送给锚点消息的用户
+ sender_info=anchor_message.message_info.user_info,
message_segment=message_segment,
- reply=anchor_message, # 回复锚点消息
+ reply=anchor_message, # 回复原始锚点
is_head=not mark_head,
is_emoji=False,
- thinking_start_time=thinking_start_time,
+ thinking_start_time=thinking_start_time, # 传递原始思考开始时间
)
- if not mark_head:
- mark_head = True
- first_bot_msg = bot_message
- message_set.add_message(bot_message)
+ try:
+ if not mark_head:
+ mark_head = True
+ first_bot_msg = bot_message # 保存第一个成功发送的消息对象
+ await self.heart_fc_sender.type_and_send_message(bot_message, type=False)
+ else:
+ await self.heart_fc_sender.type_and_send_message(bot_message, type=True)
- # Access MessageManager directly
- await message_manager.add_message(message_set)
- return first_bot_msg
+ reply_message_ids.append(part_message_id) # 记录我们生成的ID
+
+ except Exception as e:
+ logger.error(
+ f"{self.log_prefix}[Sender-{thinking_id}] 发送回复片段 {i} ({part_message_id}) 时失败: {e}"
+ )
+ # 这里可以选择是继续发送下一个片段还是中止
+
+ # 在尝试发送完所有片段后,完成原始的 thinking_id 状态
+ try:
+ await self.heart_fc_sender.complete_thinking(chat_id, thinking_id)
+ except Exception as e:
+ logger.error(f"{self.log_prefix}[Sender-{thinking_id}] 完成思考状态 {thinking_id} 时出错: {e}")
+
+ self._current_cycle.set_response_info(
+ response_text=response_set, # 保留原始文本
+ anchor_message_id=anchor_message.message_info.message_id, # 保留锚点ID
+ reply_message_ids=reply_message_ids, # 添加实际发送的ID列表
+ )
+
+ return first_bot_msg # 返回第一个成功发送的消息对象
async def _handle_emoji(self, anchor_message: Optional[MessageRecv], response_set: List[str], send_emoji: str = ""):
- """处理表情包 (尝试锚定到 anchor_message)"""
-
+ """处理表情包 (尝试锚定到 anchor_message),使用 HeartFCSender"""
if not anchor_message or not anchor_message.chat_stream:
- logger.error(f"{self._get_log_prefix()} 无法处理表情包,缺少有效的锚点消息或聊天流。")
+ logger.error(f"{self.log_prefix} 无法处理表情包,缺少有效的锚点消息或聊天流。")
return
chat = anchor_message.chat_stream
- if send_emoji:
- emoji_raw = await emoji_manager.get_emoji_for_text(send_emoji)
- else:
- emoji_text_source = "".join(response_set) if response_set else ""
- emoji_raw = await emoji_manager.get_emoji_for_text(emoji_text_source)
+ emoji_raw = await emoji_manager.get_emoji_for_text(send_emoji)
if emoji_raw:
- emoji_path, _description = emoji_raw
+ emoji_path, description = emoji_raw
+
emoji_cq = image_path_to_base64(emoji_path)
- thinking_time_point = round(time.time(), 2)
+ thinking_time_point = round(time.time(), 2) # 用于唯一ID
message_segment = Seg(type="emoji", data=emoji_cq)
bot_user_info = UserInfo(
user_id=global_config.BOT_QQ,
@@ -856,14 +1069,38 @@ class HeartFChatting:
platform=anchor_message.message_info.platform,
)
bot_message = MessageSending(
- message_id="me" + str(thinking_time_point), # 使用不同的 ID 前缀?
+ message_id="me" + str(thinking_time_point), # 表情消息的唯一ID
chat_stream=chat,
bot_user_info=bot_user_info,
sender_info=anchor_message.message_info.user_info,
message_segment=message_segment,
- reply=anchor_message, # 回复锚点消息
- is_head=False,
+ reply=anchor_message, # 回复原始锚点
+ is_head=False, # 表情通常不是头部消息
is_emoji=True,
+ # 不需要 thinking_start_time
)
- # Access MessageManager directly
- await message_manager.add_message(bot_message)
+
+ try:
+ await self.heart_fc_sender.send_and_store(bot_message)
+ except Exception as e:
+ logger.error(f"{self.log_prefix} 发送表情包 {bot_message.message_info.message_id} 时失败: {e}")
+
+ def get_cycle_history(self, last_n: Optional[int] = None) -> List[Dict[str, Any]]:
+ """获取循环历史记录
+
+ 参数:
+ last_n: 获取最近n个循环的信息,如果为None则获取所有历史记录
+
+ 返回:
+ List[Dict[str, Any]]: 循环历史记录列表
+ """
+ history = list(self._cycle_history)
+ if last_n is not None:
+ history = history[-last_n:]
+ return [cycle.to_dict() for cycle in history]
+
+ def get_last_cycle_info(self) -> Optional[Dict[str, Any]]:
+ """获取最近一个循环的信息"""
+ if self._cycle_history:
+ return self._cycle_history[-1].to_dict()
+ return None
diff --git a/src/plugins/heartFC_chat/heartFC_generator.py b/src/plugins/heartFC_chat/heartFC_generator.py
index 28329b89..43b4d87d 100644
--- a/src/plugins/heartFC_chat/heartFC_generator.py
+++ b/src/plugins/heartFC_chat/heartFC_generator.py
@@ -8,7 +8,7 @@ from .heartflow_prompt_builder import prompt_builder
from ..chat.utils import process_llm_response
from src.common.logger import get_module_logger, LogConfig, LLM_STYLE_CONFIG
from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
-from ..utils.timer_calculater import Timer
+from ..utils.timer_calculator import Timer
from src.plugins.moods.moods import MoodManager
@@ -39,6 +39,7 @@ class HeartFCGenerator:
async def generate_response(
self,
+ structured_info: str,
current_mind_info: str,
reason: str,
message: MessageRecv,
@@ -46,23 +47,15 @@ class HeartFCGenerator:
) -> Optional[List[str]]:
"""根据当前模型类型选择对应的生成函数"""
- logger.info(
- f"思考:{message.processed_plain_text[:30] + '...' if len(message.processed_plain_text) > 30 else message.processed_plain_text}"
- )
-
arousal_multiplier = MoodManager.get_instance().get_arousal_multiplier()
- with Timer() as t_generate_response:
- current_model = self.model_normal
- current_model.temperature = global_config.llm_normal["temp"] * arousal_multiplier # 激活度越高,温度越高
- model_response = await self._generate_response_with_model(
- current_mind_info, reason, message, current_model, thinking_id
- )
+ current_model = self.model_normal
+ current_model.temperature = global_config.llm_normal["temp"] * arousal_multiplier # 激活度越高,温度越高
+ model_response = await self._generate_response_with_model(
+ structured_info, current_mind_info, reason, message, current_model, thinking_id
+ )
if model_response:
- logger.info(
- f"{global_config.BOT_NICKNAME}的回复是:{model_response},生成回复时间: {t_generate_response.human_readable}"
- )
model_processed_response = await self._process_response(model_response)
return model_processed_response
@@ -71,28 +64,33 @@ class HeartFCGenerator:
return None
async def _generate_response_with_model(
- self, current_mind_info: str, reason: str, message: MessageRecv, model: LLMRequest, thinking_id: str
+ self,
+ structured_info: str,
+ current_mind_info: str,
+ reason: str,
+ message: MessageRecv,
+ model: LLMRequest,
+ thinking_id: str,
) -> str:
- sender_name = ""
-
info_catcher = info_catcher_manager.get_info_catcher(thinking_id)
- sender_name = f"<{message.chat_stream.user_info.platform}:{message.chat_stream.user_info.user_id}:{message.chat_stream.user_info.user_nickname}:{message.chat_stream.user_info.user_cardname}>"
-
- with Timer() as t_build_prompt:
+ with Timer() as _build_prompt:
prompt = await prompt_builder.build_prompt(
build_mode="focus",
reason=reason,
current_mind_info=current_mind_info,
- message_txt=message.processed_plain_text,
- sender_name=sender_name,
+ structured_info=structured_info,
+ message_txt="",
+ sender_name="",
chat_stream=message.chat_stream,
)
- logger.info(f"构建prompt时间: {t_build_prompt.human_readable}")
+ # logger.info(f"构建prompt时间: {t_build_prompt.human_readable}")
try:
content, reasoning_content, self.current_model_name = await model.generate_response(prompt)
+ logger.info(f"\nprompt:{prompt}\n生成回复{content}\n")
+
info_catcher.catch_after_llm_generated(
prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=self.current_model_name
)
@@ -103,106 +101,6 @@ class HeartFCGenerator:
return content
- async def _get_emotion_tags(self, content: str, processed_plain_text: str):
- """提取情感标签,结合立场和情绪"""
- try:
- # 构建提示词,结合回复内容、被回复的内容以及立场分析
- prompt = f"""
- 请严格根据以下对话内容,完成以下任务:
- 1. 判断回复者对被回复者观点的直接立场:
- - "支持":明确同意或强化被回复者观点
- - "反对":明确反驳或否定被回复者观点
- - "中立":不表达明确立场或无关回应
- 2. 从"开心,愤怒,悲伤,惊讶,平静,害羞,恐惧,厌恶,困惑"中选出最匹配的1个情感标签
- 3. 按照"立场-情绪"的格式直接输出结果,例如:"反对-愤怒"
- 4. 考虑回复者的人格设定为{global_config.personality_core}
-
- 对话示例:
- 被回复:「A就是笨」
- 回复:「A明明很聪明」 → 反对-愤怒
-
- 当前对话:
- 被回复:「{processed_plain_text}」
- 回复:「{content}」
-
- 输出要求:
- - 只需输出"立场-情绪"结果,不要解释
- - 严格基于文字直接表达的对立关系判断
- """
-
- # 调用模型生成结果
- result, _, _ = await self.model_sum.generate_response(prompt)
- result = result.strip()
-
- # 解析模型输出的结果
- if "-" in result:
- stance, emotion = result.split("-", 1)
- valid_stances = ["支持", "反对", "中立"]
- valid_emotions = ["开心", "愤怒", "悲伤", "惊讶", "害羞", "平静", "恐惧", "厌恶", "困惑"]
- if stance in valid_stances and emotion in valid_emotions:
- return stance, emotion # 返回有效的立场-情绪组合
- else:
- logger.debug(f"无效立场-情感组合:{result}")
- return "中立", "平静" # 默认返回中立-平静
- else:
- logger.debug(f"立场-情感格式错误:{result}")
- return "中立", "平静" # 格式错误时返回默认值
-
- except Exception as e:
- logger.debug(f"获取情感标签时出错: {e}")
- return "中立", "平静" # 出错时返回默认值
-
- async def _get_emotion_tags_with_reason(self, content: str, processed_plain_text: str, reason: str):
- """提取情感标签,结合立场和情绪"""
- try:
- # 构建提示词,结合回复内容、被回复的内容以及立场分析
- prompt = f"""
- 请严格根据以下对话内容,完成以下任务:
- 1. 判断回复者对被回复者观点的直接立场:
- - "支持":明确同意或强化被回复者观点
- - "反对":明确反驳或否定被回复者观点
- - "中立":不表达明确立场或无关回应
- 2. 从"开心,愤怒,悲伤,惊讶,平静,害羞,恐惧,厌恶,困惑"中选出最匹配的1个情感标签
- 3. 按照"立场-情绪"的格式直接输出结果,例如:"反对-愤怒"
- 4. 考虑回复者的人格设定为{global_config.personality_core}
-
- 对话示例:
- 被回复:「A就是笨」
- 回复:「A明明很聪明」 → 反对-愤怒
-
- 当前对话:
- 被回复:「{processed_plain_text}」
- 回复:「{content}」
-
- 原因:「{reason}」
-
- 输出要求:
- - 只需输出"立场-情绪"结果,不要解释
- - 严格基于文字直接表达的对立关系判断
- """
-
- # 调用模型生成结果
- result, _, _ = await self.model_sum.generate_response(prompt)
- result = result.strip()
-
- # 解析模型输出的结果
- if "-" in result:
- stance, emotion = result.split("-", 1)
- valid_stances = ["支持", "反对", "中立"]
- valid_emotions = ["开心", "愤怒", "悲伤", "惊讶", "害羞", "平静", "恐惧", "厌恶", "困惑"]
- if stance in valid_stances and emotion in valid_emotions:
- return stance, emotion # 返回有效的立场-情绪组合
- else:
- logger.debug(f"无效立场-情感组合:{result}")
- return "中立", "平静" # 默认返回中立-平静
- else:
- logger.debug(f"立场-情感格式错误:{result}")
- return "中立", "平静" # 格式错误时返回默认值
-
- except Exception as e:
- logger.debug(f"获取情感标签时出错: {e}")
- return "中立", "平静" # 出错时返回默认值
-
async def _process_response(self, content: str) -> List[str]:
"""处理响应内容,返回处理后的内容和情感标签"""
if not content:
diff --git a/src/plugins/heartFC_chat/heartFC_readme.md b/src/plugins/heartFC_chat/heartFC_readme.md
new file mode 100644
index 00000000..07bc4c63
--- /dev/null
+++ b/src/plugins/heartFC_chat/heartFC_readme.md
@@ -0,0 +1,159 @@
+# HeartFC_chat 工作原理文档
+
+HeartFC_chat 是一个基于心流理论的聊天系统,通过模拟人类的思维过程和情感变化来实现自然的对话交互。系统采用Plan-Replier-Sender循环机制,实现了智能化的对话决策和生成。
+
+## 核心工作流程
+
+### 1. 消息处理与存储 (HeartFCProcessor)
+[代码位置: src/plugins/heartFC_chat/heartflow_processor.py]
+
+消息处理器负责接收和预处理消息,主要完成以下工作:
+```mermaid
+graph TD
+ A[接收原始消息] --> B[解析为MessageRecv对象]
+ B --> C[消息缓冲处理]
+ C --> D[过滤检查]
+ D --> E[存储到数据库]
+```
+
+核心实现:
+- 消息处理入口:`process_message()` [行号: 38-215]
+ - 消息解析和缓冲:`message_buffer.start_caching_messages()` [行号: 63]
+ - 过滤检查:`_check_ban_words()`, `_check_ban_regex()` [行号: 196-215]
+ - 消息存储:`storage.store_message()` [行号: 108]
+
+### 2. 对话管理循环 (HeartFChatting)
+[代码位置: src/plugins/heartFC_chat/heartFC_chat.py]
+
+HeartFChatting是系统的核心组件,实现了完整的对话管理循环:
+
+```mermaid
+graph TD
+ A[Plan阶段] -->|决策是否回复| B[Replier阶段]
+ B -->|生成回复内容| C[Sender阶段]
+ C -->|发送消息| D[等待新消息]
+ D --> A
+```
+
+#### Plan阶段 [行号: 282-386]
+- 主要函数:`_planner()`
+- 功能实现:
+ * 获取观察信息:`observation.observe()` [行号: 297]
+ * 思维处理:`sub_mind.do_thinking_before_reply()` [行号: 301]
+ * LLM决策:使用`PLANNER_TOOL_DEFINITION`进行动作规划 [行号: 13-42]
+
+#### Replier阶段 [行号: 388-416]
+- 主要函数:`_replier_work()`
+- 调用生成器:`gpt_instance.generate_response()` [行号: 394]
+- 处理生成结果和错误情况
+
+#### Sender阶段 [行号: 418-450]
+- 主要函数:`_sender()`
+- 发送实现:
+ * 创建消息:`_create_thinking_message()` [行号: 452-477]
+ * 发送回复:`_send_response_messages()` [行号: 479-525]
+ * 处理表情:`_handle_emoji()` [行号: 527-567]
+
+### 3. 回复生成机制 (HeartFCGenerator)
+[代码位置: src/plugins/heartFC_chat/heartFC_generator.py]
+
+回复生成器负责产生高质量的回复内容:
+
+```mermaid
+graph TD
+ A[获取上下文信息] --> B[构建提示词]
+ B --> C[调用LLM生成]
+ C --> D[后处理优化]
+ D --> E[返回回复集]
+```
+
+核心实现:
+- 生成入口:`generate_response()` [行号: 39-67]
+ * 情感调节:`arousal_multiplier = MoodManager.get_instance().get_arousal_multiplier()` [行号: 47]
+ * 模型生成:`_generate_response_with_model()` [行号: 69-95]
+ * 响应处理:`_process_response()` [行号: 97-106]
+
+### 4. 提示词构建系统 (HeartFlowPromptBuilder)
+[代码位置: src/plugins/heartFC_chat/heartflow_prompt_builder.py]
+
+提示词构建器支持两种工作模式,HeartFC_chat专门使用Focus模式,而Normal模式是为normal_chat设计的:
+
+#### 专注模式 (Focus Mode) - HeartFC_chat专用
+- 实现函数:`_build_prompt_focus()` [行号: 116-141]
+- 特点:
+ * 专注于当前对话状态和思维
+ * 更强的目标导向性
+ * 用于HeartFC_chat的Plan-Replier-Sender循环
+ * 简化的上下文处理,专注于决策
+
+#### 普通模式 (Normal Mode) - Normal_chat专用
+- 实现函数:`_build_prompt_normal()` [行号: 143-215]
+- 特点:
+ * 用于normal_chat的常规对话
+ * 完整的个性化处理
+ * 关系系统集成
+ * 知识库检索:`get_prompt_info()` [行号: 217-591]
+
+HeartFC_chat的Focus模式工作流程:
+```mermaid
+graph TD
+ A[获取结构化信息] --> B[获取当前思维状态]
+ B --> C[构建专注模式提示词]
+ C --> D[用于Plan阶段决策]
+ D --> E[用于Replier阶段生成]
+```
+
+## 智能特性
+
+### 1. 对话决策机制
+- LLM决策工具定义:`PLANNER_TOOL_DEFINITION` [heartFC_chat.py 行号: 13-42]
+- 决策执行:`_planner()` [heartFC_chat.py 行号: 282-386]
+- 考虑因素:
+ * 上下文相关性
+ * 情感状态
+ * 兴趣程度
+ * 对话时机
+
+### 2. 状态管理
+[代码位置: src/plugins/heartFC_chat/heartFC_chat.py]
+- 状态机实现:`HeartFChatting`类 [行号: 44-567]
+- 核心功能:
+ * 初始化:`_initialize()` [行号: 89-112]
+ * 循环控制:`_run_pf_loop()` [行号: 192-281]
+ * 状态转换:`_handle_loop_completion()` [行号: 166-190]
+
+### 3. 回复生成策略
+[代码位置: src/plugins/heartFC_chat/heartFC_generator.py]
+- 温度调节:`current_model.temperature = global_config.llm_normal["temp"] * arousal_multiplier` [行号: 48]
+- 生成控制:`_generate_response_with_model()` [行号: 69-95]
+- 响应处理:`_process_response()` [行号: 97-106]
+
+## 系统配置
+
+### 关键参数
+- LLM配置:`model_normal` [heartFC_generator.py 行号: 32-37]
+- 过滤规则:`_check_ban_words()`, `_check_ban_regex()` [heartflow_processor.py 行号: 196-215]
+- 状态控制:`INITIAL_DURATION = 60.0` [heartFC_chat.py 行号: 11]
+
+### 优化建议
+1. 调整LLM参数:`temperature`和`max_tokens`
+2. 优化提示词模板:`init_prompt()` [heartflow_prompt_builder.py 行号: 8-115]
+3. 配置状态转换条件
+4. 维护过滤规则
+
+## 注意事项
+
+1. 系统稳定性
+- 异常处理:各主要函数都包含try-except块
+- 状态检查:`_processing_lock`确保并发安全
+- 循环控制:`_loop_active`和`_loop_task`管理
+
+2. 性能优化
+- 缓存使用:`message_buffer`系统
+- LLM调用优化:批量处理和复用
+- 异步处理:使用`asyncio`
+
+3. 质量控制
+- 日志记录:使用`get_module_logger()`
+- 错误追踪:详细的异常记录
+- 响应监控:完整的状态跟踪
diff --git a/src/plugins/heartFC_chat/heartFC_sender.py b/src/plugins/heartFC_chat/heartFC_sender.py
new file mode 100644
index 00000000..d436c668
--- /dev/null
+++ b/src/plugins/heartFC_chat/heartFC_sender.py
@@ -0,0 +1,153 @@
+# src/plugins/heartFC_chat/heartFC_sender.py
+import asyncio # 重新导入 asyncio
+from typing import Dict, Optional # 重新导入类型
+
+from src.common.logger import get_module_logger
+from ..message.api import global_api
+from ..chat.message import MessageSending, MessageThinking # 只保留 MessageSending 和 MessageThinking
+from ..storage.storage import MessageStorage
+from ..chat.utils import truncate_message
+from src.common.logger import LogConfig, SENDER_STYLE_CONFIG
+from src.plugins.chat.utils import calculate_typing_time
+
+# 定义日志配置
+sender_config = LogConfig(
+ # 使用消息发送专用样式
+ console_format=SENDER_STYLE_CONFIG["console_format"],
+ file_format=SENDER_STYLE_CONFIG["file_format"],
+)
+
+logger = get_module_logger("msg_sender", config=sender_config)
+
+
+class HeartFCSender:
+ """管理消息的注册、即时处理、发送和存储,并跟踪思考状态。"""
+
+ def __init__(self):
+ self.storage = MessageStorage()
+ # 用于存储活跃的思考消息
+ self.thinking_messages: Dict[str, Dict[str, MessageThinking]] = {}
+ self._thinking_lock = asyncio.Lock() # 保护 thinking_messages 的锁
+
+ async def send_message(self, message: MessageSending) -> None:
+ """合并后的消息发送函数,包含WS发送和日志记录"""
+ message_preview = truncate_message(message.processed_plain_text)
+
+ try:
+ # 直接调用API发送消息
+ await global_api.send_message(message)
+ logger.success(f"发送消息 '{message_preview}' 成功")
+
+ except Exception as e:
+ logger.error(f"发送消息 '{message_preview}' 失败: {str(e)}")
+ if not message.message_info.platform:
+ raise ValueError(f"未找到平台:{message.message_info.platform} 的url配置,请检查配置文件") from e
+ raise e # 重新抛出其他异常
+
+ async def register_thinking(self, thinking_message: MessageThinking):
+ """注册一个思考中的消息。"""
+ if not thinking_message.chat_stream or not thinking_message.message_info.message_id:
+ logger.error("无法注册缺少 chat_stream 或 message_id 的思考消息")
+ return
+
+ chat_id = thinking_message.chat_stream.stream_id
+ message_id = thinking_message.message_info.message_id
+
+ async with self._thinking_lock:
+ if chat_id not in self.thinking_messages:
+ self.thinking_messages[chat_id] = {}
+ if message_id in self.thinking_messages[chat_id]:
+ logger.warning(f"[{chat_id}] 尝试注册已存在的思考消息 ID: {message_id}")
+ self.thinking_messages[chat_id][message_id] = thinking_message
+ logger.debug(f"[{chat_id}] Registered thinking message: {message_id}")
+
+ async def complete_thinking(self, chat_id: str, message_id: str):
+ """完成并移除一个思考中的消息记录。"""
+ async with self._thinking_lock:
+ if chat_id in self.thinking_messages and message_id in self.thinking_messages[chat_id]:
+ del self.thinking_messages[chat_id][message_id]
+ logger.debug(f"[{chat_id}] Completed thinking message: {message_id}")
+ if not self.thinking_messages[chat_id]:
+ del self.thinking_messages[chat_id]
+ logger.debug(f"[{chat_id}] Removed empty thinking message container.")
+
+ def is_thinking(self, chat_id: str, message_id: str) -> bool:
+ """检查指定的消息 ID 是否当前正处于思考状态。"""
+ return chat_id in self.thinking_messages and message_id in self.thinking_messages[chat_id]
+
+ async def get_thinking_start_time(self, chat_id: str, message_id: str) -> Optional[float]:
+ """获取已注册思考消息的开始时间。"""
+ async with self._thinking_lock:
+ thinking_message = self.thinking_messages.get(chat_id, {}).get(message_id)
+ return thinking_message.thinking_start_time if thinking_message else None
+
+ async def type_and_send_message(self, message: MessageSending, type=False):
+ """
+ 立即处理、发送并存储单个 MessageSending 消息。
+ 调用此方法前,应先调用 register_thinking 注册对应的思考消息。
+ 此方法执行后会调用 complete_thinking 清理思考状态。
+ """
+ if not message.chat_stream:
+ logger.error("消息缺少 chat_stream,无法发送")
+ return
+ if not message.message_info or not message.message_info.message_id:
+ logger.error("消息缺少 message_info 或 message_id,无法发送")
+ return
+
+ chat_id = message.chat_stream.stream_id
+ message_id = message.message_info.message_id
+
+ try:
+ _ = message.update_thinking_time()
+
+ # --- 条件应用 set_reply 逻辑 ---
+ if message.apply_set_reply_logic and message.is_head and not message.is_private_message():
+ logger.debug(f"[{chat_id}] 应用 set_reply 逻辑: {message.processed_plain_text[:20]}...")
+ message.set_reply()
+ # --- 结束条件 set_reply ---
+
+ await message.process()
+
+ if type:
+ typing_time = calculate_typing_time(
+ input_string=message.processed_plain_text,
+ thinking_start_time=message.thinking_start_time,
+ is_emoji=message.is_emoji,
+ )
+ await asyncio.sleep(typing_time)
+
+ await self.send_message(message)
+ await self.storage.store_message(message, message.chat_stream)
+
+ except Exception as e:
+ logger.error(f"[{chat_id}] 处理或存储消息 {message_id} 时出错: {e}")
+ raise e
+ finally:
+ await self.complete_thinking(chat_id, message_id)
+
+ async def send_and_store(self, message: MessageSending):
+ """处理、发送并存储单个消息,不涉及思考状态管理。"""
+ if not message.chat_stream:
+ logger.error(f"[{message.message_info.platform or 'UnknownPlatform'}] 消息缺少 chat_stream,无法发送")
+ return
+ if not message.message_info or not message.message_info.message_id:
+ logger.error(
+ f"[{message.chat_stream.stream_id if message.chat_stream else 'UnknownStream'}] 消息缺少 message_info 或 message_id,无法发送"
+ )
+ return
+
+ chat_id = message.chat_stream.stream_id
+ message_id = message.message_info.message_id # 获取消息ID用于日志
+
+ try:
+ await message.process()
+
+ await asyncio.sleep(0.5)
+
+ await self.send_message(message) # 使用现有的发送方法
+ await self.storage.store_message(message, message.chat_stream) # 使用现有的存储方法
+
+ except Exception as e:
+ logger.error(f"[{chat_id}] 处理或存储消息 {message_id} 时出错: {e}")
+ # 重新抛出异常,让调用者知道失败了
+ raise e
diff --git a/src/plugins/heartFC_chat/heartflow_processor.py b/src/plugins/heartFC_chat/heartflow_processor.py
index 366bb125..1f771688 100644
--- a/src/plugins/heartFC_chat/heartflow_processor.py
+++ b/src/plugins/heartFC_chat/heartflow_processor.py
@@ -5,13 +5,14 @@ from ...config.config import global_config
from ..chat.message import MessageRecv
from ..storage.storage import MessageStorage
from ..chat.utils import is_mentioned_bot_in_message
-from ..message import Seg
+from maim_message import Seg
from src.heart_flow.heartflow import heartflow
from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig
from ..chat.chat_stream import chat_manager
from ..chat.message_buffer import message_buffer
-from ..utils.timer_calculater import Timer
+from ..utils.timer_calculator import Timer
from src.plugins.person_info.relationship_manager import relationship_manager
+from typing import Optional, Tuple
# 定义日志配置
processor_config = LogConfig(
@@ -22,193 +23,202 @@ logger = get_module_logger("heartflow_processor", config=processor_config)
class HeartFCProcessor:
+ """心流处理器,负责处理接收到的消息并计算兴趣度"""
+
def __init__(self):
+ """初始化心流处理器,创建消息存储实例"""
self.storage = MessageStorage()
- async def process_message(self, message_data: str) -> None:
- """处理接收到的原始消息数据,完成消息解析、缓冲、过滤、存储、兴趣度计算与更新等核心流程。
-
- 此函数是消息处理的核心入口,负责接收原始字符串格式的消息数据,并将其转化为结构化的 `MessageRecv` 对象。
- 主要执行步骤包括:
- 1. 解析 `message_data` 为 `MessageRecv` 对象,提取用户信息、群组信息等。
- 2. 将消息加入 `message_buffer` 进行缓冲处理,以应对消息轰炸或者某些人一条消息分几次发等情况。
- 3. 获取或创建对应的 `chat_stream` 和 `subheartflow` 实例,用于管理会话状态和心流。
- 4. 对消息内容进行初步处理(如提取纯文本)。
- 5. 应用全局配置中的过滤词和正则表达式,过滤不符合规则的消息。
- 6. 查询消息缓冲结果,如果消息被缓冲器拦截(例如,判断为消息轰炸的一部分),则中止后续处理。
- 7. 对于通过缓冲的消息,将其存储到 `MessageStorage` 中。
-
- 8. 调用海马体(`HippocampusManager`)计算消息内容的记忆激活率。(这部分算法后续会进行优化)
- 9. 根据是否被提及(@)和记忆激活率,计算最终的兴趣度增量。(提及的额外兴趣增幅)
- 10. 使用计算出的增量更新 `InterestManager` 中对应会话的兴趣度。
- 11. 记录处理后的消息信息及当前的兴趣度到日志。
-
- 注意:此函数本身不负责生成和发送回复。回复的决策和生成逻辑被移至 `HeartFC_Chat` 类中的监控任务,
- 该任务会根据 `InterestManager` 中的兴趣度变化来决定何时触发回复。
+ async def _handle_error(self, error: Exception, context: str, message: Optional[MessageRecv] = None) -> None:
+ """统一的错误处理函数
Args:
- message_data: str: 从消息源接收到的原始消息字符串。
+ error: 捕获到的异常
+ context: 错误发生的上下文描述
+ message: 可选的消息对象,用于记录相关消息内容
+ """
+ logger.error(f"{context}: {error}")
+ logger.error(traceback.format_exc())
+ if message and hasattr(message, "raw_message"):
+ logger.error(f"相关消息原始内容: {message.raw_message}")
+
+ async def _process_relationship(self, message: MessageRecv) -> None:
+ """处理用户关系逻辑
+
+ Args:
+ message: 消息对象,包含用户信息
+ """
+ platform = message.message_info.platform
+ user_id = message.message_info.user_info.user_id
+ nickname = message.message_info.user_info.user_nickname
+ cardname = message.message_info.user_info.user_cardname or nickname
+
+ is_known = await relationship_manager.is_known_some_one(platform, user_id)
+
+ if not is_known:
+ logger.info(f"首次认识用户: {nickname}")
+ await relationship_manager.first_knowing_some_one(platform, user_id, nickname, cardname, "")
+ elif not await relationship_manager.is_qved_name(platform, user_id):
+ logger.info(f"给用户({nickname},{cardname})取名: {nickname}")
+ await relationship_manager.first_knowing_some_one(platform, user_id, nickname, cardname, "")
+
+ async def _calculate_interest(self, message: MessageRecv) -> Tuple[float, bool]:
+ """计算消息的兴趣度
+
+ Args:
+ message: 待处理的消息对象
+
+ Returns:
+ Tuple[float, bool]: (兴趣度, 是否被提及)
+ """
+ is_mentioned, _ = is_mentioned_bot_in_message(message)
+ interested_rate = 0.0
+
+ with Timer("记忆激活"):
+ interested_rate = await HippocampusManager.get_instance().get_activate_from_text(
+ message.processed_plain_text,
+ fast_retrieval=True,
+ )
+ logger.trace(f"记忆激活率: {interested_rate:.2f}")
+
+ if is_mentioned:
+ interest_increase_on_mention = 1
+ interested_rate += interest_increase_on_mention
+
+ return interested_rate, is_mentioned
+
+ def _get_message_type(self, message: MessageRecv) -> str:
+ """获取消息类型
+
+ Args:
+ message: 消息对象
+
+ Returns:
+ str: 消息类型
+ """
+ if message.message_segment.type != "seglist":
+ return message.message_segment.type
+
+ if (
+ isinstance(message.message_segment.data, list)
+ and all(isinstance(x, Seg) for x in message.message_segment.data)
+ and len(message.message_segment.data) == 1
+ ):
+ return message.message_segment.data[0].type
+
+ return "seglist"
+
+ async def process_message(self, message_data: str) -> None:
+ """处理接收到的原始消息数据
+
+ 主要流程:
+ 1. 消息解析与初始化
+ 2. 消息缓冲处理
+ 3. 过滤检查
+ 4. 兴趣度计算
+ 5. 关系处理
+
+ Args:
+ message_data: 原始消息字符串
"""
- timing_results = {} # 初始化 timing_results
message = None
try:
+ # 1. 消息解析与初始化
message = MessageRecv(message_data)
groupinfo = message.message_info.group_info
userinfo = message.message_info.user_info
messageinfo = message.message_info
- # 消息加入缓冲池
+ # 2. 消息缓冲与流程序化
await message_buffer.start_caching_messages(message)
- # 创建聊天流
chat = await chat_manager.get_or_create_stream(
platform=messageinfo.platform,
user_info=userinfo,
group_info=groupinfo,
)
- subheartflow = await heartflow.create_subheartflow(chat.stream_id)
-
+ subheartflow = await heartflow.get_or_create_subheartflow(chat.stream_id)
message.update_chat_stream(chat)
-
- await heartflow.create_subheartflow(chat.stream_id)
-
await message.process()
- logger.trace(f"消息处理成功: {message.processed_plain_text}")
- # 过滤词/正则表达式过滤
+ # 3. 过滤检查
if self._check_ban_words(message.processed_plain_text, chat, userinfo) or self._check_ban_regex(
message.raw_message, chat, userinfo
):
return
- # 查询缓冲器结果
+ # 4. 缓冲检查
buffer_result = await message_buffer.query_buffer_result(message)
-
- # 处理缓冲器结果 (Bombing logic)
if not buffer_result:
- f_type = "seglist"
- if message.message_segment.type != "seglist":
- f_type = message.message_segment.type
- else:
- if (
- isinstance(message.message_segment.data, list)
- and all(isinstance(x, Seg) for x in message.message_segment.data)
- and len(message.message_segment.data) == 1
- ):
- f_type = message.message_segment.data[0].type
- if f_type == "text":
- logger.debug(f"触发缓冲,消息:{message.processed_plain_text}")
- elif f_type == "image":
- logger.debug("触发缓冲,表情包/图片等待中")
- elif f_type == "seglist":
- logger.debug("触发缓冲,消息列表等待中")
- return # 被缓冲器拦截,不生成回复
-
- # ---- 只有通过缓冲的消息才进行存储和后续处理 ----
-
- # 存储消息 (使用可能被缓冲器更新过的 message)
- try:
- await self.storage.store_message(message, chat)
- logger.trace(f"存储成功 (通过缓冲后): {message.processed_plain_text}")
- except Exception as e:
- logger.error(f"存储消息失败: {e}")
- logger.error(traceback.format_exc())
- # 存储失败可能仍需考虑是否继续,暂时返回
+ msg_type = self._get_message_type(message)
+ type_messages = {
+ "text": f"触发缓冲,消息:{message.processed_plain_text}",
+ "image": "触发缓冲,表情包/图片等待中",
+ "seglist": "触发缓冲,消息列表等待中",
+ }
+ logger.debug(type_messages.get(msg_type, "触发未知类型缓冲"))
return
- # 激活度计算 (使用可能被缓冲器更新过的 message.processed_plain_text)
- is_mentioned, _ = is_mentioned_bot_in_message(message)
- interested_rate = 0.0 # 默认值
- try:
- with Timer("记忆激活", timing_results):
- interested_rate = await HippocampusManager.get_instance().get_activate_from_text(
- message.processed_plain_text,
- fast_retrieval=True, # 使用更新后的文本
- )
- logger.trace(f"记忆激活率 (通过缓冲后): {interested_rate:.2f}")
- except Exception as e:
- logger.error(f"计算记忆激活率失败: {e}")
- logger.error(traceback.format_exc())
+ # 5. 消息存储
+ await self.storage.store_message(message, chat)
+ logger.trace(f"存储成功: {message.processed_plain_text}")
- # --- 修改:兴趣度更新逻辑 --- #
- if is_mentioned:
- interest_increase_on_mention = 1
- mentioned_boost = interest_increase_on_mention # 从配置获取提及增加值
- interested_rate += mentioned_boost
+ # 6. 兴趣度计算与更新
+ interested_rate, is_mentioned = await self._calculate_interest(message)
+ await subheartflow.interest_chatting.increase_interest(value=interested_rate)
+ await subheartflow.interest_chatting.add_interest_dict(message, interested_rate, is_mentioned)
- # 更新兴趣度 (调用 SubHeartflow 的方法)
- current_time = time.time()
- await subheartflow.interest_chatting.increase_interest(current_time, value=interested_rate)
-
- # 添加到 SubHeartflow 的 interest_dict,给normal_chat处理
- await subheartflow.add_interest_dict_entry(message, interested_rate, is_mentioned)
-
- # 打印消息接收和处理信息
+ # 7. 日志记录
mes_name = chat.group_info.group_name if chat.group_info else "私聊"
- current_time = time.strftime("%H:%M:%S", time.localtime(message.message_info.time))
+ current_time = time.strftime("%H点%M分%S秒", time.localtime(message.message_info.time))
logger.info(
f"[{current_time}][{mes_name}]"
- f"{chat.user_info.user_nickname}:"
+ f"{userinfo.user_nickname}:"
f"{message.processed_plain_text}"
f"[兴趣度: {interested_rate:.2f}]"
)
- try:
- is_known = await relationship_manager.is_known_some_one(
- message.message_info.platform, message.message_info.user_info.user_id
- )
- if not is_known:
- logger.info(f"首次认识用户: {message.message_info.user_info.user_nickname}")
- await relationship_manager.first_knowing_some_one(
- message.message_info.platform,
- message.message_info.user_info.user_id,
- message.message_info.user_info.user_nickname,
- message.message_info.user_info.user_cardname or message.message_info.user_info.user_nickname,
- "",
- )
- else:
- # logger.debug(f"已认识用户: {message.message_info.user_info.user_nickname}")
- if not await relationship_manager.is_qved_name(
- message.message_info.platform, message.message_info.user_info.user_id
- ):
- logger.info(f"更新已认识但未取名的用户: {message.message_info.user_info.user_nickname}")
- await relationship_manager.first_knowing_some_one(
- message.message_info.platform,
- message.message_info.user_info.user_id,
- message.message_info.user_info.user_nickname,
- message.message_info.user_info.user_cardname
- or message.message_info.user_info.user_nickname,
- "",
- )
- except Exception as e:
- logger.error(f"处理认识关系失败: {e}")
- logger.error(traceback.format_exc())
+ # 8. 关系处理
+ await self._process_relationship(message)
except Exception as e:
- logger.error(f"消息处理失败 (process_message V3): {e}")
- logger.error(traceback.format_exc())
- if message: # 记录失败的消息内容
- logger.error(f"失败消息原始内容: {message.raw_message}")
+ await self._handle_error(e, "消息处理失败", message)
def _check_ban_words(self, text: str, chat, userinfo) -> bool:
- """检查消息中是否包含过滤词"""
+ """检查消息是否包含过滤词
+
+ Args:
+ text: 待检查的文本
+ chat: 聊天对象
+ userinfo: 用户信息
+
+ Returns:
+ bool: 是否包含过滤词
+ """
for word in global_config.ban_words:
if word in text:
- logger.info(
- f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{text}"
- )
+ chat_name = chat.group_info.group_name if chat.group_info else "私聊"
+ logger.info(f"[{chat_name}]{userinfo.user_nickname}:{text}")
logger.info(f"[过滤词识别]消息中含有{word},filtered")
return True
return False
def _check_ban_regex(self, text: str, chat, userinfo) -> bool:
- """检查消息是否匹配过滤正则表达式"""
+ """检查消息是否匹配过滤正则表达式
+
+ Args:
+ text: 待检查的文本
+ chat: 聊天对象
+ userinfo: 用户信息
+
+ Returns:
+ bool: 是否匹配过滤正则
+ """
for pattern in global_config.ban_msgs_regex:
if pattern.search(text):
- logger.info(
- f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{text}"
- )
+ chat_name = chat.group_info.group_name if chat.group_info else "私聊"
+ logger.info(f"[{chat_name}]{userinfo.user_nickname}:{text}")
logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered")
return True
return False
diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py
index 1d19d1ca..584205a7 100644
--- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py
+++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py
@@ -21,19 +21,78 @@ logger = get_module_logger("prompt")
def init_prompt():
Prompt(
"""
+{info_from_tools}
{chat_target}
{chat_talking_prompt}
-现在"{sender_name}"说的:{message_txt}。引起了你的注意,你想要在群里发言发言或者回复这条消息。\n
-你的网名叫{bot_name},{prompt_personality} {prompt_identity}。
-你正在{chat_target_2},现在请你读读之前的聊天记录,然后给出日常且口语化的回复,平淡一些,
-你刚刚脑子里在想:
+现在你想要在群里发言或者回复。\n
+你需要扮演一位网名叫{bot_name}的人进行回复,这个人的特点是:"{prompt_personality} {prompt_identity}"。
+你正在{chat_target_2},现在请你读读之前的聊天记录,然后给出日常且口语化的回复,平淡一些,你可以参考贴吧,知乎或者微博的回复风格。
+看到以上聊天记录,你刚刚在想:
+
{current_mind_info}
-{reason}
-回复尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,不要回复的太有条理,可以有个性。请一次只回复一个话题,不要同时回复多个人。{prompt_ger}
-请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,尽量不要说你说过的话 ,注意只输出回复内容。
-{moderation_prompt}。注意:不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""",
+因为上述想法,你决定发言,原因是:{reason}
+
+回复尽量简短一些。请注意把握聊天内容,不要回复的太有条理,可以有个性。请一次只回复一个话题,不要同时回复多个人,不用指出你回复的是谁。{prompt_ger}
+请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,不要说你说过的话题 ,注意只输出回复内容。
+{moderation_prompt}。注意:回复不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""",
"heart_flow_prompt",
)
+
+ Prompt(
+ """
+你有以下信息可供参考:
+{structured_info}
+以上的消息是你获取到的消息,或许可以帮助你更好地回复。
+""",
+ "info_from_tools",
+ )
+
+ # Planner提示词 - 优化版
+ Prompt(
+ """你的名字是{bot_name},一名大二女生,正在QQ群聊中。需要基于以下信息决定如何参与对话:
+{structured_info_block}
+{chat_content_block}
+你的内心想法:
+{current_mind_block}
+{replan}
+
+请综合分析聊天内容和你看到的新消息,参考内心想法,使用'decide_reply_action'工具做出决策。决策时请注意:
+
+【回复原则】
+1. 不回复(no_reply)适用:
+- 话题无关/无聊/不感兴趣
+- 最后一条消息是你自己发的且无人回应你
+- 讨论你不懂的专业话题
+- 你发送了太多消息
+
+2. 文字回复(text_reply)适用:
+- 有实质性内容需要表达
+- 可以追加emoji_query表达情绪(格式:情绪描述,如"俏皮的调侃")
+- 不要追加太多表情
+
+3. 纯表情回复(emoji_reply)适用:
+- 适合用表情回应的场景
+- 需提供明确的emoji_query
+
+4. 自我对话处理:
+- 如果是自己发的消息想继续,需自然衔接
+- 避免重复或评价自己的发言
+- 不要和自己聊天
+
+【必须遵守】
+- 必须调用工具并包含action和reasoning
+- 你可以选择文字回复(text_reply),纯表情回复(emoji_reply),不回复(no_reply)
+- 选择text_reply或emoji_reply时必须提供emoji_query
+- 保持回复自然,符合日常聊天习惯""",
+ "planner_prompt",
+ )
+
+ Prompt(
+ """你原本打算{action},因为:{reasoning}
+但是你看到了新的消息,你决定重新决定行动。""",
+ "replan_prompt",
+ )
+
Prompt("你正在qq群里聊天,下面是群里在聊的内容:", "chat_target_group1")
Prompt("和群里聊天", "chat_target_group2")
Prompt("你正在和{sender_name}聊天,这是你们之前聊的内容:", "chat_target_private1")
@@ -52,13 +111,13 @@ def init_prompt():
{schedule_prompt}
{chat_target}
{chat_talking_prompt}
-现在"{sender_name}"说的:{message_txt}。引起了你的注意,你想要在群里发言发言或者回复这条消息。\n
+现在"{sender_name}"说的:{message_txt}。引起了你的注意,你想要在群里发言或者回复这条消息。\n
你的网名叫{bot_name},有人也叫你{bot_other_names},{prompt_personality}。
你正在{chat_target_2},现在请你读读之前的聊天记录,{mood_prompt},然后给出日常且口语化的回复,平淡一些,
尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,不要回复的太有条理,可以有个性。{prompt_ger}
-请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,尽量不要说你说过的话
+请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,不要浮夸,平淡一些 ,不要重复自己说过的话。
请注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。
-{moderation_prompt}不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""",
+{moderation_prompt}不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。,只输出回复内容""",
"reasoning_prompt_main",
)
Prompt(
@@ -79,18 +138,28 @@ class PromptBuilder:
self.activate_messages = ""
async def build_prompt(
- self, build_mode, reason, current_mind_info, message_txt: str, sender_name: str = "某人", chat_stream=None
+ self,
+ build_mode,
+ reason,
+ current_mind_info,
+ structured_info,
+ message_txt: str,
+ sender_name: str = "某人",
+ chat_stream=None,
) -> Optional[tuple[str, str]]:
if build_mode == "normal":
return await self._build_prompt_normal(chat_stream, message_txt, sender_name)
elif build_mode == "focus":
- return await self._build_prompt_focus(reason, current_mind_info, chat_stream, message_txt, sender_name)
+ return await self._build_prompt_focus(
+ reason,
+ current_mind_info,
+ structured_info,
+ chat_stream,
+ )
return None
- async def _build_prompt_focus(
- self, reason, current_mind_info, chat_stream, message_txt: str, sender_name: str = "某人"
- ) -> tuple[str, str]:
+ async def _build_prompt_focus(self, reason, current_mind_info, structured_info, chat_stream) -> tuple[str, str]:
individuality = Individuality.get_instance()
prompt_personality = individuality.get_prompt(type="personality", x_person=2, level=1)
prompt_identity = individuality.get_prompt(type="identity", x_person=2, level=1)
@@ -113,30 +182,10 @@ class PromptBuilder:
message_list_before_now,
replace_bot_name=True,
merge_messages=False,
- timestamp_mode="relative",
+ timestamp_mode="normal",
read_mark=0.0,
)
- # 关键词检测与反应
- keywords_reaction_prompt = ""
- for rule in global_config.keywords_reaction_rules:
- if rule.get("enable", False):
- if any(keyword in message_txt.lower() for keyword in rule.get("keywords", [])):
- logger.info(
- f"检测到以下关键词之一:{rule.get('keywords', [])},触发反应:{rule.get('reaction', '')}"
- )
- keywords_reaction_prompt += rule.get("reaction", "") + ","
- else:
- for pattern in rule.get("regex", []):
- result = pattern.search(message_txt)
- if result:
- reaction = rule.get("reaction", "")
- for name, content in result.groupdict().items():
- reaction = reaction.replace(f"[{name}]", content)
- logger.info(f"匹配到以下正则表达式:{pattern},触发反应:{reaction}")
- keywords_reaction_prompt += reaction + ","
- break
-
# 中文高手(新加的好玩功能)
prompt_ger = ""
if random.random() < 0.04:
@@ -144,16 +193,22 @@ class PromptBuilder:
if random.random() < 0.02:
prompt_ger += "你喜欢用反问句"
+ if structured_info:
+ structured_info_prompt = await global_prompt_manager.format_prompt(
+ "info_from_tools", structured_info=structured_info
+ )
+ else:
+ structured_info_prompt = ""
+
logger.debug("开始构建prompt")
prompt = await global_prompt_manager.format_prompt(
"heart_flow_prompt",
+ info_from_tools=structured_info_prompt,
chat_target=await global_prompt_manager.get_prompt_async("chat_target_group1")
if chat_in_group
else await global_prompt_manager.get_prompt_async("chat_target_private1"),
chat_talking_prompt=chat_talking_prompt,
- sender_name=sender_name,
- message_txt=message_txt,
bot_name=global_config.BOT_NICKNAME,
prompt_personality=prompt_personality,
prompt_identity=prompt_identity,
@@ -162,7 +217,6 @@ class PromptBuilder:
else await global_prompt_manager.get_prompt_async("chat_target_private2"),
current_mind_info=current_mind_info,
reason=reason,
- keywords_reaction_prompt=keywords_reaction_prompt,
prompt_ger=prompt_ger,
moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"),
)
diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py
index c020b407..6687421e 100644
--- a/src/plugins/heartFC_chat/normal_chat.py
+++ b/src/plugins/heartFC_chat/normal_chat.py
@@ -6,18 +6,18 @@ from typing import List, Optional # 导入 Optional
from ..moods.moods import MoodManager
from ...config.config import global_config
-from ..chat.emoji_manager import emoji_manager
+from ..emoji_system.emoji_manager import emoji_manager
from .normal_chat_generator import NormalChatGenerator
from ..chat.message import MessageSending, MessageRecv, MessageThinking, MessageSet
from ..chat.message_sender import message_manager
from ..chat.utils_image import image_path_to_base64
from ..willing.willing_manager import willing_manager
-from ..message import UserInfo, Seg
+from maim_message import UserInfo, Seg
from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig
from src.plugins.chat.chat_stream import ChatStream, chat_manager
from src.plugins.person_info.relationship_manager import relationship_manager
from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
-from src.plugins.utils.timer_calculater import Timer
+from src.plugins.utils.timer_calculator import Timer
# 定义日志配置
chat_config = LogConfig(
@@ -164,14 +164,13 @@ class NormalChat:
)
self.mood_manager.update_mood_from_emotion(emotion, global_config.mood_intensity_factor)
- async def _find_interested_message(self) -> None:
+ async def _reply_interested_message(self) -> None:
"""
后台任务方法,轮询当前实例关联chat的兴趣消息
通常由start_monitoring_interest()启动
"""
while True:
- await asyncio.sleep(1) # 每秒检查一次
-
+ await asyncio.sleep(0.5) # 每秒检查一次
# 检查任务是否已被取消
if self._chat_task is None or self._chat_task.cancelled():
logger.info(f"[{self.stream_name}] 兴趣监控任务被取消或置空,退出")
@@ -353,36 +352,27 @@ class NormalChat:
async def start_chat(self):
"""为此 NormalChat 实例关联的 ChatStream 启动聊天任务(如果尚未运行)。"""
if self._chat_task is None or self._chat_task.done():
- logger.info(f"[{self.stream_name}] 启动聊天任务...")
- task = asyncio.create_task(self._find_interested_message())
+ task = asyncio.create_task(self._reply_interested_message())
task.add_done_callback(lambda t: self._handle_task_completion(t)) # 回调现在是实例方法
self._chat_task = task
- # 改为实例方法, 移除 stream_id 参数
def _handle_task_completion(self, task: asyncio.Task):
- """兴趣监控任务完成时的回调函数。"""
- # 检查完成的任务是否是当前实例的任务
+ """任务完成回调处理"""
if task is not self._chat_task:
- logger.warning(f"[{self.stream_name}] 收到一个未知或过时任务的完成回调。")
+ logger.warning(f"[{self.stream_name}] 收到未知任务回调")
return
-
try:
- # 检查任务是否因异常而结束
- exception = task.exception()
- if exception:
- logger.error(f"[{self.stream_name}] 兴趣监控任务因异常结束: {exception}")
- logger.error(traceback.format_exc()) # 记录完整的 traceback
- # else: # 减少日志
- # logger.info(f"[{self.stream_name}] 兴趣监控任务正常结束。")
+ if exc := task.exception():
+ logger.error(f"[{self.stream_name}] 任务异常: {exc}")
+ logger.error(traceback.format_exc())
except asyncio.CancelledError:
- logger.info(f"[{self.stream_name}] 兴趣监控任务被取消。")
+ logger.info(f"[{self.stream_name}] 任务已取消")
except Exception as e:
- logger.error(f"[{self.stream_name}] 处理任务完成回调时出错: {e}")
+ logger.error(f"[{self.stream_name}] 回调处理错误: {e}")
finally:
- # 标记任务已完成/移除
- if self._chat_task is task: # 再次确认是当前任务
+ if self._chat_task is task:
self._chat_task = None
- logger.debug(f"[{self.stream_name}] 聊天任务已被标记为完成/移除。")
+ logger.debug(f"[{self.stream_name}] 任务清理完成")
# 改为实例方法, 移除 stream_id 参数
async def stop_chat(self):
@@ -402,3 +392,17 @@ class NormalChat:
# 确保任务状态更新,即使等待出错 (回调函数也会尝试更新)
if self._chat_task is task:
self._chat_task = None
+
+ # 清理所有未处理的思考消息
+ try:
+ container = await message_manager.get_container(self.stream_id)
+ if container:
+ # 查找并移除所有 MessageThinking 类型的消息
+ thinking_messages = [msg for msg in container.messages[:] if isinstance(msg, MessageThinking)]
+ if thinking_messages:
+ for msg in thinking_messages:
+ container.messages.remove(msg)
+ logger.info(f"[{self.stream_name}] 清理了 {len(thinking_messages)} 条未处理的思考消息。")
+ except Exception as e:
+ logger.error(f"[{self.stream_name}] 清理思考消息时出错: {e}")
+ logger.error(traceback.format_exc())
diff --git a/src/plugins/heartFC_chat/normal_chat_generator.py b/src/plugins/heartFC_chat/normal_chat_generator.py
index 07635baf..52d0f446 100644
--- a/src/plugins/heartFC_chat/normal_chat_generator.py
+++ b/src/plugins/heartFC_chat/normal_chat_generator.py
@@ -5,7 +5,7 @@ from ...config.config import global_config
from ..chat.message import MessageThinking
from .heartflow_prompt_builder import prompt_builder
from ..chat.utils import process_llm_response
-from ..utils.timer_calculater import Timer
+from ..utils.timer_calculator import Timer
from src.common.logger import get_module_logger, LogConfig, LLM_STYLE_CONFIG
from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
@@ -83,6 +83,7 @@ class NormalChatGenerator:
build_mode="normal",
reason="",
current_mind_info="",
+ structured_info="",
message_txt=message.processed_plain_text,
sender_name=sender_name,
chat_stream=message.chat_stream,
diff --git a/src/plugins/message/__init__.py b/src/plugins/message/__init__.py
index 286ef231..b5eed4d4 100644
--- a/src/plugins/message/__init__.py
+++ b/src/plugins/message/__init__.py
@@ -3,23 +3,8 @@
__version__ = "0.1.0"
from .api import global_api
-from .message_base import (
- Seg,
- GroupInfo,
- UserInfo,
- FormatInfo,
- TemplateInfo,
- BaseMessageInfo,
- MessageBase,
-)
+
__all__ = [
- "Seg",
"global_api",
- "GroupInfo",
- "UserInfo",
- "FormatInfo",
- "TemplateInfo",
- "BaseMessageInfo",
- "MessageBase",
]
diff --git a/src/plugins/message/api.py b/src/plugins/message/api.py
index fb51539e..e82ab98f 100644
--- a/src/plugins/message/api.py
+++ b/src/plugins/message/api.py
@@ -1,250 +1,6 @@
-from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
-from typing import Dict, Any, Callable, List, Set, Optional
-from src.common.logger import get_module_logger
-from src.plugins.message.message_base import MessageBase
from src.common.server import global_server
-import aiohttp
-import asyncio
-import uvicorn
import os
-import traceback
-
-logger = get_module_logger("api")
-
-
-class BaseMessageHandler:
- """消息处理基类"""
-
- def __init__(self):
- self.message_handlers: List[Callable] = []
- self.background_tasks = set()
-
- def register_message_handler(self, handler: Callable):
- """注册消息处理函数"""
- self.message_handlers.append(handler)
-
- async def process_message(self, message: Dict[str, Any]):
- """处理单条消息"""
- tasks = []
- for handler in self.message_handlers:
- try:
- tasks.append(handler(message))
- except Exception as e:
- logger.error(f"消息处理出错: {str(e)}")
- logger.error(traceback.format_exc())
- # 不抛出异常,而是记录错误并继续处理其他消息
- continue
- if tasks:
- await asyncio.gather(*tasks, return_exceptions=True)
-
- async def _handle_message(self, message: Dict[str, Any]):
- """后台处理单个消息"""
- try:
- await self.process_message(message)
- except Exception as e:
- raise RuntimeError(str(e)) from e
-
-
-class MessageServer(BaseMessageHandler):
- """WebSocket服务端"""
-
- _class_handlers: List[Callable] = [] # 类级别的消息处理器
-
- def __init__(
- self,
- host: str = "0.0.0.0",
- port: int = 18000,
- enable_token=False,
- app: Optional[FastAPI] = None,
- path: str = "/ws",
- ):
- super().__init__()
- # 将类级别的处理器添加到实例处理器中
- self.message_handlers.extend(self._class_handlers)
- self.host = host
- self.port = port
- self.path = path
- self.app = app or FastAPI()
- self.own_app = app is None # 标记是否使用自己创建的app
- self.active_websockets: Set[WebSocket] = set()
- self.platform_websockets: Dict[str, WebSocket] = {} # 平台到websocket的映射
- self.valid_tokens: Set[str] = set()
- self.enable_token = enable_token
- self._setup_routes()
- self._running = False
-
- def _setup_routes(self):
- @self.app.post("/api/message")
- async def handle_message(message: Dict[str, Any]):
- try:
- # 创建后台任务处理消息
- asyncio.create_task(self._handle_message(message))
- return {"status": "success"}
- except Exception as e:
- raise HTTPException(status_code=500, detail=str(e)) from e
-
- @self.app.websocket("/ws")
- async def websocket_endpoint(websocket: WebSocket):
- headers = dict(websocket.headers)
- token = headers.get("authorization")
- platform = headers.get("platform", "default") # 获取platform标识
- if self.enable_token:
- if not token or not await self.verify_token(token):
- await websocket.close(code=1008, reason="Invalid or missing token")
- return
-
- await websocket.accept()
- self.active_websockets.add(websocket)
-
- # 添加到platform映射
- if platform not in self.platform_websockets:
- self.platform_websockets[platform] = websocket
-
- try:
- while True:
- message = await websocket.receive_json()
- # print(f"Received message: {message}")
- asyncio.create_task(self._handle_message(message))
- except WebSocketDisconnect:
- self._remove_websocket(websocket, platform)
- except Exception as e:
- self._remove_websocket(websocket, platform)
- raise RuntimeError(str(e)) from e
- finally:
- self._remove_websocket(websocket, platform)
-
- @classmethod
- def register_class_handler(cls, handler: Callable):
- """注册类级别的消息处理器"""
- if handler not in cls._class_handlers:
- cls._class_handlers.append(handler)
-
- def register_message_handler(self, handler: Callable):
- """注册实例级别的消息处理器"""
- if handler not in self.message_handlers:
- self.message_handlers.append(handler)
-
- async def verify_token(self, token: str) -> bool:
- if not self.enable_token:
- return True
- return token in self.valid_tokens
-
- def add_valid_token(self, token: str):
- self.valid_tokens.add(token)
-
- def remove_valid_token(self, token: str):
- self.valid_tokens.discard(token)
-
- def run_sync(self):
- """同步方式运行服务器"""
- if not self.own_app:
- raise RuntimeError("当使用外部FastAPI实例时,请使用该实例的运行方法")
- uvicorn.run(self.app, host=self.host, port=self.port)
-
- async def run(self):
- """异步方式运行服务器"""
- self._running = True
- try:
- if self.own_app:
- # 如果使用自己的 FastAPI 实例,运行 uvicorn 服务器
- # 禁用 uvicorn 默认日志和访问日志
- config = uvicorn.Config(
- self.app, host=self.host, port=self.port, loop="asyncio", log_config=None, access_log=False
- )
- self.server = uvicorn.Server(config)
- await self.server.serve()
- else:
- # 如果使用外部 FastAPI 实例,保持运行状态以处理消息
- while self._running:
- await asyncio.sleep(1)
- except KeyboardInterrupt:
- await self.stop()
- raise
- except Exception as e:
- await self.stop()
- raise RuntimeError(f"服务器运行错误: {str(e)}") from e
- finally:
- await self.stop()
-
- async def start_server(self):
- """启动服务器的异步方法"""
- if not self._running:
- self._running = True
- await self.run()
-
- async def stop(self):
- """停止服务器"""
- # 清理platform映射
- self.platform_websockets.clear()
-
- # 取消所有后台任务
- for task in self.background_tasks:
- task.cancel()
- # 等待所有任务完成
- await asyncio.gather(*self.background_tasks, return_exceptions=True)
- self.background_tasks.clear()
-
- # 关闭所有WebSocket连接
- for websocket in self.active_websockets:
- await websocket.close()
- self.active_websockets.clear()
-
- if hasattr(self, "server") and self.own_app:
- self._running = False
- # 正确关闭 uvicorn 服务器
- self.server.should_exit = True
- await self.server.shutdown()
- # 等待服务器完全停止
- if hasattr(self.server, "started") and self.server.started:
- await self.server.main_loop()
- # 清理处理程序
- self.message_handlers.clear()
-
- def _remove_websocket(self, websocket: WebSocket, platform: str):
- """从所有集合中移除websocket"""
- if websocket in self.active_websockets:
- self.active_websockets.remove(websocket)
- if platform in self.platform_websockets:
- if self.platform_websockets[platform] == websocket:
- del self.platform_websockets[platform]
-
- async def broadcast_message(self, message: Dict[str, Any]):
- disconnected = set()
- for websocket in self.active_websockets:
- try:
- await websocket.send_json(message)
- except Exception:
- disconnected.add(websocket)
- for websocket in disconnected:
- self.active_websockets.remove(websocket)
-
- async def broadcast_to_platform(self, platform: str, message: Dict[str, Any]):
- """向指定平台的所有WebSocket客户端广播消息"""
- if platform not in self.platform_websockets:
- raise ValueError(f"平台:{platform} 未连接")
-
- disconnected = set()
- try:
- await self.platform_websockets[platform].send_json(message)
- except Exception:
- disconnected.add(self.platform_websockets[platform])
-
- # 清理断开的连接
- for websocket in disconnected:
- self._remove_websocket(websocket, platform)
-
- async def send_message(self, message: MessageBase):
- await self.broadcast_to_platform(message.message_info.platform, message.to_dict())
-
- @staticmethod
- async def send_message_rest(url: str, data: Dict[str, Any]) -> Dict[str, Any]:
- """发送消息到指定端点"""
- async with aiohttp.ClientSession() as session:
- try:
- async with session.post(url, json=data, headers={"Content-Type": "application/json"}) as response:
- return await response.json()
- except Exception as e:
- raise e
+from maim_message import MessageServer
global_api = MessageServer(host=os.environ["HOST"], port=int(os.environ["PORT"]), app=global_server.get_app())
diff --git a/src/plugins/message/message_base.py b/src/plugins/message/message_base.py
deleted file mode 100644
index b853d469..00000000
--- a/src/plugins/message/message_base.py
+++ /dev/null
@@ -1,247 +0,0 @@
-from dataclasses import dataclass, asdict
-from typing import List, Optional, Union, Dict
-
-
-@dataclass
-class Seg:
- """消息片段类,用于表示消息的不同部分
-
- Attributes:
- type: 片段类型,可以是 'text'、'image'、'seglist' 等
- data: 片段的具体内容
- - 对于 text 类型,data 是字符串
- - 对于 image 类型,data 是 base64 字符串
- - 对于 seglist 类型,data 是 Seg 列表
- """
-
- type: str
- data: Union[str, List["Seg"]]
-
- # def __init__(self, type: str, data: Union[str, List['Seg']],):
- # """初始化实例,确保字典和属性同步"""
- # # 先初始化字典
- # self.type = type
- # self.data = data
-
- @classmethod
- def from_dict(cls, data: Dict) -> "Seg":
- """从字典创建Seg实例"""
- type = data.get("type")
- data = data.get("data")
- if type == "seglist":
- data = [Seg.from_dict(seg) for seg in data]
- return cls(type=type, data=data)
-
- def to_dict(self) -> Dict:
- """转换为字典格式"""
- result = {"type": self.type}
- if self.type == "seglist":
- result["data"] = [seg.to_dict() for seg in self.data]
- else:
- result["data"] = self.data
- return result
-
-
-@dataclass
-class GroupInfo:
- """群组信息类"""
-
- platform: Optional[str] = None
- group_id: Optional[int] = None
- group_name: Optional[str] = None # 群名称
-
- def to_dict(self) -> Dict:
- """转换为字典格式"""
- return {k: v for k, v in asdict(self).items() if v is not None}
-
- @classmethod
- def from_dict(cls, data: Dict) -> "GroupInfo":
- """从字典创建GroupInfo实例
-
- Args:
- data: 包含必要字段的字典
-
- Returns:
- GroupInfo: 新的实例
- """
- if data.get("group_id") is None:
- return None
- return cls(
- platform=data.get("platform"), group_id=data.get("group_id"), group_name=data.get("group_name", None)
- )
-
-
-@dataclass
-class UserInfo:
- """用户信息类"""
-
- platform: Optional[str] = None
- user_id: Optional[int] = None
- user_nickname: Optional[str] = None # 用户昵称
- user_cardname: Optional[str] = None # 用户群昵称
-
- def to_dict(self) -> Dict:
- """转换为字典格式"""
- return {k: v for k, v in asdict(self).items() if v is not None}
-
- @classmethod
- def from_dict(cls, data: Dict) -> "UserInfo":
- """从字典创建UserInfo实例
-
- Args:
- data: 包含必要字段的字典
-
- Returns:
- UserInfo: 新的实例
- """
- return cls(
- platform=data.get("platform"),
- user_id=data.get("user_id"),
- user_nickname=data.get("user_nickname", None),
- user_cardname=data.get("user_cardname", None),
- )
-
-
-@dataclass
-class FormatInfo:
- """格式信息类"""
-
- """
- 目前maimcore可接受的格式为text,image,emoji
- 可发送的格式为text,emoji,reply
- """
-
- content_format: Optional[str] = None
- accept_format: Optional[str] = None
-
- def to_dict(self) -> Dict:
- """转换为字典格式"""
- return {k: v for k, v in asdict(self).items() if v is not None}
-
- @classmethod
- def from_dict(cls, data: Dict) -> "FormatInfo":
- """从字典创建FormatInfo实例
- Args:
- data: 包含必要字段的字典
- Returns:
- FormatInfo: 新的实例
- """
- return cls(
- content_format=data.get("content_format"),
- accept_format=data.get("accept_format"),
- )
-
-
-@dataclass
-class TemplateInfo:
- """模板信息类"""
-
- template_items: Optional[Dict] = None
- template_name: Optional[str] = None
- template_default: bool = True
-
- def to_dict(self) -> Dict:
- """转换为字典格式"""
- return {k: v for k, v in asdict(self).items() if v is not None}
-
- @classmethod
- def from_dict(cls, data: Dict) -> "TemplateInfo":
- """从字典创建TemplateInfo实例
- Args:
- data: 包含必要字段的字典
- Returns:
- TemplateInfo: 新的实例
- """
- return cls(
- template_items=data.get("template_items"),
- template_name=data.get("template_name"),
- template_default=data.get("template_default", True),
- )
-
-
-@dataclass
-class BaseMessageInfo:
- """消息信息类"""
-
- platform: Optional[str] = None
- message_id: Union[str, int, None] = None
- time: Optional[float] = None
- group_info: Optional[GroupInfo] = None
- user_info: Optional[UserInfo] = None
- format_info: Optional[FormatInfo] = None
- template_info: Optional[TemplateInfo] = None
- additional_config: Optional[dict] = None
-
- def to_dict(self) -> Dict:
- """转换为字典格式"""
- result = {}
- for field, value in asdict(self).items():
- if value is not None:
- if isinstance(value, (GroupInfo, UserInfo, FormatInfo, TemplateInfo)):
- result[field] = value.to_dict()
- else:
- result[field] = value
- return result
-
- @classmethod
- def from_dict(cls, data: Dict) -> "BaseMessageInfo":
- """从字典创建BaseMessageInfo实例
-
- Args:
- data: 包含必要字段的字典
-
- Returns:
- BaseMessageInfo: 新的实例
- """
- group_info = GroupInfo.from_dict(data.get("group_info", {}))
- user_info = UserInfo.from_dict(data.get("user_info", {}))
- format_info = FormatInfo.from_dict(data.get("format_info", {}))
- template_info = TemplateInfo.from_dict(data.get("template_info", {}))
- return cls(
- platform=data.get("platform"),
- message_id=data.get("message_id"),
- time=data.get("time"),
- additional_config=data.get("additional_config", None),
- group_info=group_info,
- user_info=user_info,
- format_info=format_info,
- template_info=template_info,
- )
-
-
-@dataclass
-class MessageBase:
- """消息类"""
-
- message_info: BaseMessageInfo
- message_segment: Seg
- raw_message: Optional[str] = None # 原始消息,包含未解析的cq码
-
- def to_dict(self) -> Dict:
- """转换为字典格式
-
- Returns:
- Dict: 包含所有非None字段的字典,其中:
- - message_info: 转换为字典格式
- - message_segment: 转换为字典格式
- - raw_message: 如果存在则包含
- """
- result = {"message_info": self.message_info.to_dict(), "message_segment": self.message_segment.to_dict()}
- if self.raw_message is not None:
- result["raw_message"] = self.raw_message
- return result
-
- @classmethod
- def from_dict(cls, data: Dict) -> "MessageBase":
- """从字典创建MessageBase实例
-
- Args:
- data: 包含必要字段的字典
-
- Returns:
- MessageBase: 新的实例
- """
- message_info = BaseMessageInfo.from_dict(data.get("message_info", {}))
- message_segment = Seg.from_dict(data.get("message_segment", {}))
- raw_message = data.get("raw_message", None)
- return cls(message_info=message_info, message_segment=message_segment, raw_message=raw_message)
diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py
index e2ec7ac3..2cab7b62 100644
--- a/src/plugins/models/utils_model.py
+++ b/src/plugins/models/utils_model.py
@@ -710,6 +710,8 @@ class LLMRequest:
usage = None # 初始化usage变量,避免未定义错误
reasoning_content = ""
content = ""
+ tool_calls = None # 初始化工具调用变量
+
async for line_bytes in response.content:
try:
line = line_bytes.decode("utf-8").strip()
@@ -731,11 +733,20 @@ class LLMRequest:
if delta_content is None:
delta_content = ""
accumulated_content += delta_content
+
+ # 提取工具调用信息
+ if "tool_calls" in delta:
+ if tool_calls is None:
+ tool_calls = delta["tool_calls"]
+ else:
+ # 合并工具调用信息
+ tool_calls.extend(delta["tool_calls"])
+
# 检测流式输出文本是否结束
finish_reason = chunk["choices"][0].get("finish_reason")
if delta.get("reasoning_content", None):
reasoning_content += delta["reasoning_content"]
- if finish_reason == "stop":
+ if finish_reason == "stop" or finish_reason == "tool_calls":
chunk_usage = chunk.get("usage", None)
if chunk_usage:
usage = chunk_usage
@@ -763,16 +774,19 @@ class LLMRequest:
if think_match:
reasoning_content = think_match.group(1).strip()
content = re.sub(r".*?", "", content, flags=re.DOTALL).strip()
+
+ # 构建消息对象
+ message = {
+ "content": content,
+ "reasoning_content": reasoning_content,
+ }
+
+ # 如果有工具调用,添加到消息中
+ if tool_calls:
+ message["tool_calls"] = tool_calls
+
result = {
- "choices": [
- {
- "message": {
- "content": content,
- "reasoning_content": reasoning_content,
- # 流式输出可能没有工具调用,此处不需要添加tool_calls字段
- }
- }
- ],
+ "choices": [{"message": message}],
"usage": usage,
}
return result
@@ -1046,6 +1060,7 @@ class LLMRequest:
# 只有当tool_calls存在且不为空时才返回
if tool_calls:
+ logger.debug(f"检测到工具调用: {tool_calls}")
return content, reasoning_content, tool_calls
else:
return content, reasoning_content
@@ -1109,8 +1124,31 @@ class LLMRequest:
response = await self._execute_request(endpoint="/chat/completions", payload=data, prompt=prompt)
# 原样返回响应,不做处理
+
return response
+ async def generate_response_tool_async(self, prompt: str, tools: list, **kwargs) -> Union[str, Tuple]:
+ """异步方式根据输入的提示生成模型的响应"""
+ # 构建请求体,不硬编码max_tokens
+ data = {
+ "model": self.model_name,
+ "messages": [{"role": "user", "content": prompt}],
+ **self.params,
+ **kwargs,
+ "tools": tools,
+ }
+
+ logger.debug(f"向模型 {self.model_name} 发送工具调用请求,包含 {len(tools)} 个工具")
+ response = await self._execute_request(endpoint="/chat/completions", payload=data, prompt=prompt)
+ # 检查响应是否包含工具调用
+ if isinstance(response, tuple) and len(response) == 3:
+ content, reasoning_content, tool_calls = response
+ logger.debug(f"收到工具调用响应,包含 {len(tool_calls) if tool_calls else 0} 个工具调用")
+ return content, reasoning_content, tool_calls
+ else:
+ logger.debug("收到普通响应,无工具调用")
+ return response
+
async def get_embedding(self, text: str) -> Union[list, None]:
"""异步方法:获取文本的embedding向量
diff --git a/src/plugins/moods/moods.py b/src/plugins/moods/moods.py
index e3fb377c..eea2177f 100644
--- a/src/plugins/moods/moods.py
+++ b/src/plugins/moods/moods.py
@@ -256,7 +256,7 @@ class MoodManager:
def print_mood_status(self) -> None:
"""打印当前情绪状态"""
logger.info(
- f"[情绪状态]愉悦度: {self.current_mood.valence:.2f}, "
+ f"愉悦度: {self.current_mood.valence:.2f}, "
f"唤醒度: {self.current_mood.arousal:.2f}, "
f"心情: {self.current_mood.text}"
)
diff --git a/src/plugins/person_info/person_info.py b/src/plugins/person_info/person_info.py
index e4f4004e..1ec9f6d0 100644
--- a/src/plugins/person_info/person_info.py
+++ b/src/plugins/person_info/person_info.py
@@ -53,7 +53,7 @@ person_info_default = {
# "impression" : None,
# "gender" : Unkown,
"konw_time": 0,
- "msg_interval": 3000,
+ "msg_interval": 2000,
"msg_interval_list": [],
} # 个人信息的各项与默认值在此定义,以下处理会自动创建/补全每一项
@@ -384,18 +384,30 @@ class PersonInfoManager:
if delta > 0:
time_interval.append(delta)
- time_interval = [t for t in time_interval if 500 <= t <= 8000]
- if len(time_interval) >= 30:
+ time_interval = [t for t in time_interval if 200 <= t <= 8000]
+ # --- 修改后的逻辑 ---
+ # 数据量检查 (至少需要 30 条有效间隔,并且足够进行头尾截断)
+ if len(time_interval) >= 30 + 10: # 至少30条有效+头尾各5条
time_interval.sort()
- # 画图(log)
+ # 画图(log) - 这部分保留
msg_interval_map = True
log_dir = Path("logs/person_info")
log_dir.mkdir(parents=True, exist_ok=True)
plt.figure(figsize=(10, 6))
- time_series = pd.Series(time_interval)
- plt.hist(time_series, bins=50, density=True, alpha=0.4, color="pink", label="Histogram")
- time_series.plot(kind="kde", color="mediumpurple", linewidth=1, label="Density")
+ # 使用截断前的数据画图,更能反映原始分布
+ time_series_original = pd.Series(time_interval)
+ plt.hist(
+ time_series_original,
+ bins=50,
+ density=True,
+ alpha=0.4,
+ color="pink",
+ label="Histogram (Original Filtered)",
+ )
+ time_series_original.plot(
+ kind="kde", color="mediumpurple", linewidth=1, label="Density (Original Filtered)"
+ )
plt.grid(True, alpha=0.2)
plt.xlim(0, 8000)
plt.title(f"Message Interval Distribution (User: {person_id[:8]}...)")
@@ -405,15 +417,24 @@ class PersonInfoManager:
img_path = log_dir / f"interval_distribution_{person_id[:8]}.png"
plt.savefig(img_path)
plt.close()
- # 画图
+ # 画图结束
- q25, q75 = np.percentile(time_interval, [25, 75])
- iqr = q75 - q25
- filtered = [x for x in time_interval if (q25 - 1.5 * iqr) <= x <= (q75 + 1.5 * iqr)]
+ # 去掉头尾各 5 个数据点
+ trimmed_interval = time_interval[5:-5]
- msg_interval = int(round(np.percentile(filtered, 80)))
- await self.update_one_field(person_id, "msg_interval", msg_interval)
- logger.trace(f"用户{person_id}的msg_interval已经被更新为{msg_interval}")
+ # 计算截断后数据的 37% 分位数
+ if trimmed_interval: # 确保截断后列表不为空
+ msg_interval = int(round(np.percentile(trimmed_interval, 37)))
+ # 更新数据库
+ await self.update_one_field(person_id, "msg_interval", msg_interval)
+ logger.trace(f"用户{person_id}的msg_interval通过头尾截断和37分位数更新为{msg_interval}")
+ else:
+ logger.trace(f"用户{person_id}截断后数据为空,无法计算msg_interval")
+ else:
+ logger.trace(
+ f"用户{person_id}有效消息间隔数量 ({len(time_interval)}) 不足进行推断 (需要至少 {30 + 10} 条)"
+ )
+ # --- 修改结束 ---
except Exception as e:
logger.trace(f"用户{person_id}消息间隔计算失败: {type(e).__name__}: {str(e)}")
continue
diff --git a/src/plugins/utils/chat_message_builder.py b/src/plugins/utils/chat_message_builder.py
index d822263d..f510365f 100644
--- a/src/plugins/utils/chat_message_builder.py
+++ b/src/plugins/utils/chat_message_builder.py
@@ -168,7 +168,10 @@ async def _build_readable_messages_internal(
user_info = msg.get("user_info", {})
platform = user_info.get("platform")
user_id = user_info.get("user_id")
- user_nickname = user_info.get("nickname")
+
+ user_nickname = user_info.get("user_nickname")
+ user_cardname = user_info.get("user_cardname")
+
timestamp = msg.get("time")
content = msg.get("processed_plain_text", "") # 默认空字符串
@@ -186,7 +189,12 @@ async def _build_readable_messages_internal(
# 如果 person_name 未设置,则使用消息中的 nickname 或默认名称
if not person_name:
- person_name = user_nickname
+ if user_cardname:
+ person_name = f"昵称:{user_cardname}"
+ elif user_nickname:
+ person_name = f"{user_nickname}"
+ else:
+ person_name = "某人"
message_details.append((timestamp, person_name, content))
@@ -303,7 +311,7 @@ async def build_readable_messages(
)
readable_read_mark = translate_timestamp_to_human_readable(read_mark, mode=timestamp_mode)
- read_mark_line = f"\n--- 以上消息已读 (标记时间: {readable_read_mark}) ---\n"
+ read_mark_line = f"\n--- 以上消息是你已经思考过的内容已读 (标记时间: {readable_read_mark}) ---\n--- 请关注以下未读的新消息---\n"
# 组合结果,确保空部分不引入多余的标记或换行
if formatted_before and formatted_after:
diff --git a/src/plugins/utils/json_utils.py b/src/plugins/utils/json_utils.py
new file mode 100644
index 00000000..bf4b0839
--- /dev/null
+++ b/src/plugins/utils/json_utils.py
@@ -0,0 +1,301 @@
+import json
+import logging
+from typing import Any, Dict, TypeVar, List, Union, Callable, Tuple
+
+# 定义类型变量用于泛型类型提示
+T = TypeVar("T")
+
+# 获取logger
+logger = logging.getLogger("json_utils")
+
+
+def safe_json_loads(json_str: str, default_value: T = None) -> Union[Any, T]:
+ """
+ 安全地解析JSON字符串,出错时返回默认值
+
+ 参数:
+ json_str: 要解析的JSON字符串
+ default_value: 解析失败时返回的默认值
+
+ 返回:
+ 解析后的Python对象,或在解析失败时返回default_value
+ """
+ if not json_str:
+ return default_value
+
+ try:
+ return json.loads(json_str)
+ except json.JSONDecodeError as e:
+ logger.error(f"JSON解析失败: {e}, JSON字符串: {json_str[:100]}...")
+ return default_value
+ except Exception as e:
+ logger.error(f"JSON解析过程中发生意外错误: {e}")
+ return default_value
+
+
+def extract_tool_call_arguments(tool_call: Dict[str, Any], default_value: Dict[str, Any] = None) -> Dict[str, Any]:
+ """
+ 从LLM工具调用对象中提取参数
+
+ 参数:
+ tool_call: 工具调用对象字典
+ default_value: 解析失败时返回的默认值
+
+ 返回:
+ 解析后的参数字典,或在解析失败时返回default_value
+ """
+ default_result = default_value or {}
+
+ if not tool_call or not isinstance(tool_call, dict):
+ logger.error(f"无效的工具调用对象: {tool_call}")
+ return default_result
+
+ try:
+ # 提取function参数
+ function_data = tool_call.get("function", {})
+ if not function_data or not isinstance(function_data, dict):
+ logger.error(f"工具调用缺少function字段或格式不正确: {tool_call}")
+ return default_result
+
+ # 提取arguments
+ arguments_str = function_data.get("arguments", "{}")
+ if not arguments_str:
+ return default_result
+
+ # 解析JSON
+ return safe_json_loads(arguments_str, default_result)
+
+ except Exception as e:
+ logger.error(f"提取工具调用参数时出错: {e}")
+ return default_result
+
+
+def get_json_value(
+ json_obj: Dict[str, Any], key_path: str, default_value: T = None, transform_func: Callable[[Any], T] = None
+) -> Union[Any, T]:
+ """
+ 从JSON对象中按照路径提取值,支持点表示法路径,如"data.items.0.name"
+
+ 参数:
+ json_obj: JSON对象(已解析的字典)
+ key_path: 键路径,使用点表示法,如"data.items.0.name"
+ default_value: 获取失败时返回的默认值
+ transform_func: 可选的转换函数,用于对获取的值进行转换
+
+ 返回:
+ 路径指向的值,或在获取失败时返回default_value
+ """
+ if not json_obj or not key_path:
+ return default_value
+
+ try:
+ # 分割路径
+ keys = key_path.split(".")
+ current = json_obj
+
+ # 遍历路径
+ for key in keys:
+ # 处理数组索引
+ if key.isdigit() and isinstance(current, list):
+ index = int(key)
+ if 0 <= index < len(current):
+ current = current[index]
+ else:
+ return default_value
+ # 处理字典键
+ elif isinstance(current, dict):
+ if key in current:
+ current = current[key]
+ else:
+ return default_value
+ else:
+ return default_value
+
+ # 应用转换函数(如果提供)
+ if transform_func and current is not None:
+ return transform_func(current)
+ return current
+ except Exception as e:
+ logger.error(f"从JSON获取值时出错: {e}, 路径: {key_path}")
+ return default_value
+
+
+def safe_json_dumps(obj: Any, default_value: str = "{}", ensure_ascii: bool = False, pretty: bool = False) -> str:
+ """
+ 安全地将Python对象序列化为JSON字符串
+
+ 参数:
+ obj: 要序列化的Python对象
+ default_value: 序列化失败时返回的默认值
+ ensure_ascii: 是否确保ASCII编码(默认False,允许中文等非ASCII字符)
+ pretty: 是否美化输出JSON
+
+ 返回:
+ 序列化后的JSON字符串,或在序列化失败时返回default_value
+ """
+ try:
+ indent = 2 if pretty else None
+ return json.dumps(obj, ensure_ascii=ensure_ascii, indent=indent)
+ except TypeError as e:
+ logger.error(f"JSON序列化失败(类型错误): {e}")
+ return default_value
+ except Exception as e:
+ logger.error(f"JSON序列化过程中发生意外错误: {e}")
+ return default_value
+
+
+def merge_json_objects(*objects: Dict[str, Any]) -> Dict[str, Any]:
+ """
+ 合并多个JSON对象(字典)
+
+ 参数:
+ *objects: 要合并的JSON对象(字典)
+
+ 返回:
+ 合并后的字典,后面的对象会覆盖前面对象的相同键
+ """
+ result = {}
+ for obj in objects:
+ if obj and isinstance(obj, dict):
+ result.update(obj)
+ return result
+
+
+def normalize_llm_response(response: Any, log_prefix: str = "") -> Tuple[bool, List[Any], str]:
+ """
+ 标准化LLM响应格式,将各种格式(如元组)转换为统一的列表格式
+
+ 参数:
+ response: 原始LLM响应
+ log_prefix: 日志前缀
+
+ 返回:
+ 元组 (成功标志, 标准化后的响应列表, 错误消息)
+ """
+ # 检查是否为None
+ if response is None:
+ return False, [], "LLM响应为None"
+
+ # 记录原始类型
+ logger.debug(f"{log_prefix}LLM响应原始类型: {type(response).__name__}")
+
+ # 将元组转换为列表
+ if isinstance(response, tuple):
+ logger.debug(f"{log_prefix}将元组响应转换为列表")
+ response = list(response)
+
+ # 确保是列表类型
+ if not isinstance(response, list):
+ return False, [], f"无法处理的LLM响应类型: {type(response).__name__}"
+
+ # 处理工具调用部分(如果存在)
+ if len(response) == 3:
+ content, reasoning, tool_calls = response
+
+ # 将工具调用部分转换为列表(如果是元组)
+ if isinstance(tool_calls, tuple):
+ logger.debug(f"{log_prefix}将工具调用元组转换为列表")
+ tool_calls = list(tool_calls)
+ response[2] = tool_calls
+
+ return True, response, ""
+
+
+def process_llm_tool_calls(response: List[Any], log_prefix: str = "") -> Tuple[bool, List[Dict[str, Any]], str]:
+ """
+ 处理并提取LLM响应中的工具调用列表
+
+ 参数:
+ response: 标准化后的LLM响应列表
+ log_prefix: 日志前缀
+
+ 返回:
+ 元组 (成功标志, 工具调用列表, 错误消息)
+ """
+ # 确保响应格式正确
+ if len(response) != 3:
+ return False, [], f"LLM响应元素数量不正确: 预期3个元素,实际{len(response)}个"
+
+ # 提取工具调用部分
+ tool_calls = response[2]
+
+ # 检查工具调用是否有效
+ if tool_calls is None:
+ return False, [], "工具调用部分为None"
+
+ if not isinstance(tool_calls, list):
+ return False, [], f"工具调用部分不是列表: {type(tool_calls).__name__}"
+
+ if len(tool_calls) == 0:
+ return False, [], "工具调用列表为空"
+
+ # 检查工具调用是否格式正确
+ valid_tool_calls = []
+ for i, tool_call in enumerate(tool_calls):
+ if not isinstance(tool_call, dict):
+ logger.warning(f"{log_prefix}工具调用[{i}]不是字典: {type(tool_call).__name__}")
+ continue
+
+ if tool_call.get("type") != "function":
+ logger.warning(f"{log_prefix}工具调用[{i}]不是函数类型: {tool_call.get('type', '未知')}")
+ continue
+
+ if "function" not in tool_call or not isinstance(tool_call["function"], dict):
+ logger.warning(f"{log_prefix}工具调用[{i}]缺少function字段或格式不正确")
+ continue
+
+ valid_tool_calls.append(tool_call)
+
+ # 检查是否有有效的工具调用
+ if not valid_tool_calls:
+ return False, [], "没有找到有效的工具调用"
+
+ return True, valid_tool_calls, ""
+
+
+def process_llm_tool_response(
+ response: Any, expected_tool_name: str = None, log_prefix: str = ""
+) -> Tuple[bool, Dict[str, Any], str]:
+ """
+ 处理LLM返回的工具调用响应,进行常见错误检查并提取参数
+
+ 参数:
+ response: LLM的响应,预期是[content, reasoning, tool_calls]格式的列表或元组
+ expected_tool_name: 预期的工具名称,如不指定则不检查
+ log_prefix: 日志前缀,用于标识日志来源
+
+ 返回:
+ 三元组(成功标志, 参数字典, 错误描述)
+ - 如果成功解析,返回(True, 参数字典, "")
+ - 如果解析失败,返回(False, {}, 错误描述)
+ """
+ # 使用新的标准化函数
+ success, normalized_response, error_msg = normalize_llm_response(response, log_prefix)
+ if not success:
+ return False, {}, error_msg
+
+ # 使用新的工具调用处理函数
+ success, valid_tool_calls, error_msg = process_llm_tool_calls(normalized_response, log_prefix)
+ if not success:
+ return False, {}, error_msg
+
+ # 检查是否有工具调用
+ if not valid_tool_calls:
+ return False, {}, "没有有效的工具调用"
+
+ # 获取第一个工具调用
+ tool_call = valid_tool_calls[0]
+
+ # 检查工具名称(如果提供了预期名称)
+ if expected_tool_name:
+ actual_name = tool_call.get("function", {}).get("name")
+ if actual_name != expected_tool_name:
+ return False, {}, f"工具名称不匹配: 预期'{expected_tool_name}',实际'{actual_name}'"
+
+ # 提取并解析参数
+ try:
+ arguments = extract_tool_call_arguments(tool_call, {})
+ return True, arguments, ""
+ except Exception as e:
+ logger.error(f"{log_prefix}解析工具参数时出错: {e}")
+ return False, {}, f"解析参数失败: {str(e)}"
diff --git a/src/plugins/utils/timer_calculater.py b/src/plugins/utils/timer_calculator.py
similarity index 100%
rename from src/plugins/utils/timer_calculater.py
rename to src/plugins/utils/timer_calculator.py
diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml
index e4e2a2a8..afb65e89 100644
--- a/template/bot_config_template.toml
+++ b/template/bot_config_template.toml
@@ -1,5 +1,5 @@
[inner]
-version = "1.4.0"
+version = "1.5.0"
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
#如果你想要修改配置文件,请在修改后将version的值进行变更
@@ -81,12 +81,8 @@ model_normal_probability = 0.3 # 麦麦回答时选择一般模型 模型的概
reply_trigger_threshold = 3.0 # 心流聊天触发阈值,越低越容易进入心流聊天
probability_decay_factor_per_second = 0.2 # 概率衰减因子,越大衰减越快,越高越容易退出心流聊天
default_decay_rate_per_second = 0.98 # 默认衰减率,越大衰减越快,越高越难进入心流聊天
-initial_duration = 60 # 初始持续时间,越大心流聊天持续的时间越长
sub_heart_flow_stop_time = 500 # 子心流停止时间,超过这个时间没有回复,子心流会停止,间隔 单位秒
-# sub_heart_flow_update_interval = 60
-# sub_heart_flow_freeze_time = 100
-# heart_flow_update_interval = 600
observation_context_size = 20 # 心流观察到的最长上下文大小,超过这个值的上下文会被压缩
compressed_length = 5 # 不能大于observation_context_size,心流上下文压缩的最短压缩长度,超过心流观察到的上下文长度,会压缩,最短压缩长度为5
@@ -122,11 +118,12 @@ mentioned_bot_inevitable_reply = false # 提及 bot 必然回复
at_bot_inevitable_reply = false # @bot 必然回复
[emoji]
-max_emoji_num = 90 # 表情包最大数量
+max_emoji_num = 40 # 表情包最大数量
max_reach_deletion = true # 开启则在达到最大数量时删除表情包,关闭则达到最大数量时不删除,只是不会继续收集表情包
-check_interval = 30 # 检查表情包(注册,破损,删除)的时间间隔(分钟)
-auto_save = true # 是否保存表情包和图片
-
+check_interval = 10 # 检查表情包(注册,破损,删除)的时间间隔(分钟)
+save_pic = false # 是否保存图片
+save_emoji = false # 是否保存表情包
+steal_emoji = true # 是否偷取表情包,让麦麦可以发送她保存的这些表情包
enable_check = false # 是否启用表情包过滤,只有符合该要求的表情包才会被保存
check_prompt = "符合公序良俗" # 表情包过滤要求,只有符合该要求的表情包才会被保存
@@ -180,11 +177,12 @@ word_replace_rate=0.006 # 整词替换概率
enable_response_splitter = true # 是否启用回复分割器
response_max_length = 256 # 回复允许的最大长度
response_max_sentence_num = 4 # 回复允许的最大句子数
+enable_kaomoji_protection = false # 是否启用颜文字保护
[remote] #发送统计信息,主要是看全球有多少只麦麦
enable = true
-[experimental] #实验性功能,不一定完善或者根本不能用
+[experimental] #实验性功能
enable_friend_chat = false # 是否启用好友聊天
pfc_chatting = false # 是否启用PFC聊天,该功能仅作用于私聊,与回复模式独立
@@ -245,6 +243,29 @@ provider = "SILICONFLOW"
pri_in = 0.35
pri_out = 0.35
+
+
+[model.llm_observation] #观察模型,压缩聊天内容,建议用免费的
+# name = "Pro/Qwen/Qwen2.5-7B-Instruct"
+name = "Qwen/Qwen2.5-7B-Instruct"
+provider = "SILICONFLOW"
+pri_in = 0
+pri_out = 0
+
+[model.llm_sub_heartflow] #子心流:认真水群时,生成麦麦的内心想法
+name = "Qwen/Qwen2.5-72B-Instruct"
+provider = "SILICONFLOW"
+pri_in = 4.13
+pri_out = 4.13
+temp = 0.7 #模型的温度,新V3建议0.1-0.3
+
+
+[model.llm_plan] #决策模型:认真水群时,负责决定麦麦该做什么
+name = "Qwen/Qwen2.5-32B-Instruct"
+provider = "SILICONFLOW"
+pri_in = 1.26
+pri_out = 1.26
+
#嵌入模型
[model.embedding] #嵌入
@@ -253,23 +274,39 @@ provider = "SILICONFLOW"
pri_in = 0
pri_out = 0
-[model.llm_observation] #观察模型,建议用免费的:建议使用qwen2.5 7b
-# name = "Pro/Qwen/Qwen2.5-7B-Instruct"
-name = "Qwen/Qwen2.5-7B-Instruct"
-provider = "SILICONFLOW"
-pri_in = 0
-pri_out = 0
-[model.llm_sub_heartflow] #子心流:建议使用V3级别
+#私聊PFC:需要开启PFC功能,默认三个模型均为硅基流动v3,如果需要支持多人同时私聊或频繁调用,建议把其中的一个或两个换成官方v3或其它模型,以免撞到429
+
+#PFC决策模型
+[model.llm_PFC_action_planner]
+name = "Pro/deepseek-ai/DeepSeek-V3"
+provider = "SILICONFLOW"
+temp = 0.3
+pri_in = 2
+pri_out = 8
+
+#PFC聊天模型
+[model.llm_PFC_chat]
+name = "Pro/deepseek-ai/DeepSeek-V3"
+provider = "SILICONFLOW"
+temp = 0.3
+pri_in = 2
+pri_out = 8
+
+#PFC检查模型
+[model.llm_PFC_reply_checker]
name = "Pro/deepseek-ai/DeepSeek-V3"
provider = "SILICONFLOW"
pri_in = 2
pri_out = 8
-temp = 0.2 #模型的温度,新V3建议0.1-0.3
-[model.llm_heartflow] #心流:建议使用qwen2.5 32b
+
+#此模型暂时没有使用!!
+#此模型暂时没有使用!!
+#此模型暂时没有使用!!
+[model.llm_heartflow] #心流
# name = "Pro/Qwen/Qwen2.5-7B-Instruct"
name = "Qwen/Qwen2.5-32B-Instruct"
provider = "SILICONFLOW"
pri_in = 1.26
-pri_out = 1.26
+pri_out = 1.26
\ No newline at end of file
diff --git a/tool_call_benchmark.py b/tool_call_benchmark.py
new file mode 100644
index 00000000..a3e28273
--- /dev/null
+++ b/tool_call_benchmark.py
@@ -0,0 +1,351 @@
+import asyncio
+import time
+from src.plugins.models.utils_model import LLMRequest
+from src.config.config import global_config
+from src.do_tool.tool_use import ToolUser
+import statistics
+import json
+
+
+async def run_test(test_name, test_function, iterations=5):
+ """
+ 运行指定次数的测试并计算平均响应时间
+
+ 参数:
+ test_name: 测试名称
+ test_function: 要执行的测试函数
+ iterations: 测试迭代次数
+
+ 返回:
+ 测试结果统计
+ """
+ print(f"开始 {test_name} 测试({iterations}次迭代)...")
+ times = []
+ responses = []
+
+ for i in range(iterations):
+ print(f" 运行第 {i + 1}/{iterations} 次测试...")
+ start_time = time.time()
+ response = await test_function()
+ end_time = time.time()
+ elapsed = end_time - start_time
+ times.append(elapsed)
+ responses.append(response)
+ print(f" - 耗时: {elapsed:.2f}秒")
+
+ results = {
+ "平均耗时": statistics.mean(times),
+ "最短耗时": min(times),
+ "最长耗时": max(times),
+ "标准差": statistics.stdev(times) if len(times) > 1 else 0,
+ "所有耗时": times,
+ "响应结果": responses,
+ }
+
+ return results
+
+
+async def test_with_tool_calls():
+ """使用工具调用的LLM请求测试"""
+ # 创建LLM模型实例
+ llm_model = LLMRequest(
+ model=global_config.llm_sub_heartflow,
+ # model = global_config.llm_tool_use,
+ # temperature=global_config.llm_sub_heartflow["temp"],
+ max_tokens=800,
+ request_type="benchmark_test",
+ )
+
+ # 创建工具实例
+ tool_instance = ToolUser()
+ tools = tool_instance._define_tools()
+
+ # 简单的测试提示词
+ prompt = "请分析当前天气情况,并查询今日历史上的重要事件。并且3.9和3.11谁比较大?请使用适当的工具来获取这些信息。"
+ prompt = """
+ 你的名字是麦麦,你包容开放,情绪敏感,有时候有些搞怪幽默, 是一个学习心理学和脑科学的女大学生,现在在读大二,你会刷贴吧,有时候会喜欢说一些奇怪的话,喜欢刷小红书
+刚刚你的内心想法是:漂移菌提到罐罐被吃完了,可以顺着这个梗继续玩一下,比如假装委屈"那今晚的加班费是不是也要被吃掉了"或者"猫娘罢工警告"。不过薯薯和薯宝之前已经接了不少梗,漂移菌刚刚也参与了,可能话题热度还在,可以再互动一下。如果没人接话,或许可以问问大家有没有遇到过类似"代码写完但奖励被吃掉"的搞笑职场经历,换个轻松的话题方向。
+
+暂时不需要使用工具。
+-----------------------------------
+现在是2025-04-25 17:38:37,你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容:
+2025-04-25 17:34:08麦麦(你) 说:[表达了:顽皮、嬉戏。];
+2025-04-25 17:34:39漂移菌 说:@麦麦。(id:3936257206) 你是一只猫娘;
+2025-04-25 17:34:42薯宝 说:🤣;
+2025-04-25 17:34:43麦麦(你) 说:行啊 工资分我一半;
+2025-04-25 17:34:43麦麦(你) 说:我帮你写bug;
+2025-04-25 17:34:43麦麦(你) 说:[表达了:悲伤、绝望、无奈、无力];
+2025-04-25 17:34:53薯薯 说:?;
+2025-04-25 17:35:03既文横 说:麦麦,你是一只猫娘程序员,猫娘是不需要工资;
+2025-04-25 17:35:20薯宝 说:[图片:图片内容:一只卡通风格的灰色猫咪,眼睛闭着,表情显得很平静。图片下方有"死了"两个字。
+
+图片含义猜测:这可能是一个幽默的表达,用来形容某人或某事处于非常平静的状态,仿佛已经"死"了一样。] hfc这周,真能出来吗...;
+2025-04-25 17:35:34薯宝 说:[表情包:搞笑、滑稽、讽刺、幽默];
+2025-04-25 17:36:25麦麦(你) 说:喵喵;
+2025-04-25 17:36:25麦麦(你) 说:代码写完了;
+2025-04-25 17:36:25麦麦(你) 说:罐罐拿来;
+2025-04-25 17:36:25麦麦(你) 说:[表达了:悲伤、绝望、无奈、无力];
+2025-04-25 17:36:41薯薯 说:好可爱;
+2025-04-25 17:37:05薯薯 说:脑补出来认真营业了一天等待主人发放奖励的小猫咪;
+2025-04-25 17:37:25薯宝 说:敷衍营业(bushi);
+2025-04-25 17:37:54漂移菌 说:回复麦麦。的消息(罐罐拿来),说:猫娘我昨晚上太饿吃完了;
+
+--- 以上消息已读 (标记时间: 2025-04-25 17:37:54) ---
+--- 以下新消息未读---
+2025-04-25 17:38:29麦麦(你) 说:那今晚的猫条是不是也要被克扣了(盯——);
+2025-04-25 17:38:29麦麦(你) 说:[表达了:幽默,自嘲,无奈,父子关系,编程笑话];
+
+你现在当前心情:平静。
+现在请你生成你的内心想法,要求思考群里正在进行的话题,之前大家聊过的话题,群里成员的关系。请你思考,要不要对群里的话题进行回复,以及如何对群聊内容进行回复
+回复的要求是:不要总是重复自己提到过的话题,如果你要回复,最好只回复一个人的一个话题
+如果最后一条消息是你自己发的,观察到的内容只有你自己的发言,并且之后没有人回复你,不要回复。如果聊天记录中最新的消息是你自己发送的,并且你还想继续回复,你应该紧紧衔接你发送的消息,进行话题的深入,补充,或追问等等。请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要回复自己的发言
+现在请你先输出想法,生成你在这个聊天中的想法,在原来的想法上尝试新的话题,不要分点输出,文字不要浮夸在输出完想法后,请你思考应该使用什么工具。工具可以帮你取得一些你不知道的信息,或者进行一些操作。如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。"""
+
+ # 发送带有工具调用的请求
+ response = await llm_model.generate_response_tool_async(prompt=prompt, tools=tools)
+
+ result_info = {}
+
+ # 简单处理工具调用结果
+ if len(response) == 3:
+ content, reasoning_content, tool_calls = response
+ tool_calls_count = len(tool_calls) if tool_calls else 0
+ print(f" 工具调用请求生成了 {tool_calls_count} 个工具调用")
+
+ # 输出内容和工具调用详情
+ print("\n 生成的内容:")
+ print(f" {content[:200]}..." if len(content) > 200 else f" {content}")
+
+ if tool_calls:
+ print("\n 工具调用详情:")
+ for i, tool_call in enumerate(tool_calls):
+ tool_name = tool_call["function"]["name"]
+ tool_params = tool_call["function"].get("arguments", {})
+ print(f" - 工具 {i + 1}: {tool_name}")
+ print(
+ f" 参数: {json.dumps(tool_params, ensure_ascii=False)[:100]}..."
+ if len(json.dumps(tool_params, ensure_ascii=False)) > 100
+ else f" 参数: {json.dumps(tool_params, ensure_ascii=False)}"
+ )
+
+ result_info = {"内容": content, "推理内容": reasoning_content, "工具调用": tool_calls}
+ else:
+ content, reasoning_content = response
+ print(" 工具调用请求未生成任何工具调用")
+ print("\n 生成的内容:")
+ print(f" {content[:200]}..." if len(content) > 200 else f" {content}")
+
+ result_info = {"内容": content, "推理内容": reasoning_content, "工具调用": []}
+
+ return result_info
+
+
+async def test_without_tool_calls():
+ """不使用工具调用的LLM请求测试"""
+ # 创建LLM模型实例
+ llm_model = LLMRequest(
+ model=global_config.llm_sub_heartflow,
+ temperature=global_config.llm_sub_heartflow["temp"],
+ max_tokens=800,
+ request_type="benchmark_test",
+ )
+
+ # 简单的测试提示词(与工具调用相同,以便公平比较)
+ prompt = """
+ 你的名字是麦麦,你包容开放,情绪敏感,有时候有些搞怪幽默, 是一个学习心理学和脑科学的女大学生,现在在读大二,你会刷贴吧,有时候会喜欢说一些奇怪的话,喜欢刷小红书
+刚刚你的内心想法是:漂移菌提到罐罐被吃完了,可以顺着这个梗继续玩一下,比如假装委屈"那今晚的加班费是不是也要被吃掉了"或者"猫娘罢工警告"。不过薯薯和薯宝之前已经接了不少梗,漂移菌刚刚也参与了,可能话题热度还在,可以再互动一下。如果没人接话,或许可以问问大家有没有遇到过类似"代码写完但奖励被吃掉"的搞笑职场经历,换个轻松的话题方向。
+
+暂时不需要使用工具。
+-----------------------------------
+现在是2025-04-25 17:38:37,你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容:
+2025-04-25 17:34:08麦麦(你) 说:[表达了:顽皮、嬉戏。];
+2025-04-25 17:34:39漂移菌 说:@麦麦。(id:3936257206) 你是一只猫娘;
+2025-04-25 17:34:42薯宝 说:🤣;
+2025-04-25 17:34:43麦麦(你) 说:行啊 工资分我一半;
+2025-04-25 17:34:43麦麦(你) 说:我帮你写bug;
+2025-04-25 17:34:43麦麦(你) 说:[表达了:悲伤、绝望、无奈、无力];
+2025-04-25 17:34:53薯薯 说:?;
+2025-04-25 17:35:03既文横 说:麦麦,你是一只猫娘程序员,猫娘是不需要工资;
+2025-04-25 17:35:20薯宝 说:[图片:图片内容:一只卡通风格的灰色猫咪,眼睛闭着,表情显得很平静。图片下方有"死了"两个字。
+
+图片含义猜测:这可能是一个幽默的表达,用来形容某人或某事处于非常平静的状态,仿佛已经"死"了一样。] hfc这周,真能出来吗...;
+2025-04-25 17:35:34薯宝 说:[表情包:搞笑、滑稽、讽刺、幽默];
+2025-04-25 17:36:25麦麦(你) 说:喵喵;
+2025-04-25 17:36:25麦麦(你) 说:代码写完了;
+2025-04-25 17:36:25麦麦(你) 说:罐罐拿来;
+2025-04-25 17:36:25麦麦(你) 说:[表达了:悲伤、绝望、无奈、无力];
+2025-04-25 17:36:41薯薯 说:好可爱;
+2025-04-25 17:37:05薯薯 说:脑补出来认真营业了一天等待主人发放奖励的小猫咪;
+2025-04-25 17:37:25薯宝 说:敷衍营业(bushi);
+2025-04-25 17:37:54漂移菌 说:回复麦麦。的消息(罐罐拿来),说:猫娘我昨晚上太饿吃完了;
+
+--- 以上消息已读 (标记时间: 2025-04-25 17:37:54) ---
+--- 以下新消息未读---
+2025-04-25 17:38:29麦麦(你) 说:那今晚的猫条是不是也要被克扣了(盯——);
+2025-04-25 17:38:29麦麦(你) 说:[表达了:幽默,自嘲,无奈,父子关系,编程笑话];
+
+你现在当前心情:平静。
+现在请你生成你的内心想法,要求思考群里正在进行的话题,之前大家聊过的话题,群里成员的关系。请你思考,要不要对群里的话题进行回复,以及如何对群聊内容进行回复
+回复的要求是:不要总是重复自己提到过的话题,如果你要回复,最好只回复一个人的一个话题
+如果最后一条消息是你自己发的,观察到的内容只有你自己的发言,并且之后没有人回复你,不要回复。如果聊天记录中最新的消息是你自己发送的,并且你还想继续回复,你应该紧紧衔接你发送的消息,进行话题的深入,补充,或追问等等。请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要回复自己的发言
+现在请你先输出想法,生成你在这个聊天中的想法,在原来的想法上尝试新的话题,不要分点输出,文字不要浮夸在输出完想法后,请你思考应该使用什么工具。工具可以帮你取得一些你不知道的信息,或者进行一些操作。如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。"""
+ # 发送不带工具调用的请求
+ response, reasoning_content = await llm_model.generate_response_async(prompt)
+
+ # 输出生成的内容
+ print("\n 生成的内容:")
+ print(f" {response[:200]}..." if len(response) > 200 else f" {response}")
+
+ result_info = {"内容": response, "推理内容": reasoning_content, "工具调用": []}
+
+ return result_info
+
+
+async def run_alternating_tests(iterations=5):
+ """
+ 交替运行两种测试方法,每种方法运行指定次数
+
+ 参数:
+ iterations: 每种测试方法运行的次数
+
+ 返回:
+ 包含两种测试方法结果的元组
+ """
+ print(f"开始交替测试(每种方法{iterations}次)...")
+
+ # 初始化结果列表
+ times_without_tools = []
+ times_with_tools = []
+ responses_without_tools = []
+ responses_with_tools = []
+
+ for i in range(iterations):
+ print(f"\n第 {i + 1}/{iterations} 轮交替测试")
+
+ # 不使用工具的测试
+ print("\n 执行不使用工具调用的测试...")
+ start_time = time.time()
+ response = await test_without_tool_calls()
+ end_time = time.time()
+ elapsed = end_time - start_time
+ times_without_tools.append(elapsed)
+ responses_without_tools.append(response)
+ print(f" - 耗时: {elapsed:.2f}秒")
+
+ # 使用工具的测试
+ print("\n 执行使用工具调用的测试...")
+ start_time = time.time()
+ response = await test_with_tool_calls()
+ end_time = time.time()
+ elapsed = end_time - start_time
+ times_with_tools.append(elapsed)
+ responses_with_tools.append(response)
+ print(f" - 耗时: {elapsed:.2f}秒")
+
+ # 计算统计数据
+ results_without_tools = {
+ "平均耗时": statistics.mean(times_without_tools),
+ "最短耗时": min(times_without_tools),
+ "最长耗时": max(times_without_tools),
+ "标准差": statistics.stdev(times_without_tools) if len(times_without_tools) > 1 else 0,
+ "所有耗时": times_without_tools,
+ "响应结果": responses_without_tools,
+ }
+
+ results_with_tools = {
+ "平均耗时": statistics.mean(times_with_tools),
+ "最短耗时": min(times_with_tools),
+ "最长耗时": max(times_with_tools),
+ "标准差": statistics.stdev(times_with_tools) if len(times_with_tools) > 1 else 0,
+ "所有耗时": times_with_tools,
+ "响应结果": responses_with_tools,
+ }
+
+ return results_without_tools, results_with_tools
+
+
+async def main():
+ """主测试函数"""
+ print("=" * 50)
+ print("LLM工具调用与普通请求性能比较测试")
+ print("=" * 50)
+
+ # 设置测试迭代次数
+ iterations = 10
+
+ # 执行交替测试
+ results_without_tools, results_with_tools = await run_alternating_tests(iterations)
+
+ # 显示结果比较
+ print("\n" + "=" * 50)
+ print("测试结果比较")
+ print("=" * 50)
+
+ print("\n不使用工具调用:")
+ for key, value in results_without_tools.items():
+ if key == "所有耗时":
+ print(f" {key}: {[f'{t:.2f}秒' for t in value]}")
+ elif key == "响应结果":
+ print(f" {key}: [内容已省略,详见结果文件]")
+ else:
+ print(f" {key}: {value:.2f}秒")
+
+ print("\n使用工具调用:")
+ for key, value in results_with_tools.items():
+ if key == "所有耗时":
+ print(f" {key}: {[f'{t:.2f}秒' for t in value]}")
+ elif key == "响应结果":
+ tool_calls_counts = [len(res.get("工具调用", [])) for res in value]
+ print(f" {key}: [内容已省略,详见结果文件]")
+ print(f" 工具调用数量: {tool_calls_counts}")
+ else:
+ print(f" {key}: {value:.2f}秒")
+
+ # 计算差异百分比
+ diff_percent = ((results_with_tools["平均耗时"] / results_without_tools["平均耗时"]) - 1) * 100
+ print(f"\n工具调用比普通请求平均耗时相差: {diff_percent:.2f}%")
+
+ # 保存结果到JSON文件
+ results = {
+ "测试时间": time.strftime("%Y-%m-%d %H:%M:%S"),
+ "测试迭代次数": iterations,
+ "不使用工具调用": {
+ k: (v if k != "所有耗时" else [float(f"{t:.2f}") for t in v])
+ for k, v in results_without_tools.items()
+ if k != "响应结果"
+ },
+ "不使用工具调用_详细响应": [
+ {
+ "内容摘要": resp["内容"][:200] + "..." if len(resp["内容"]) > 200 else resp["内容"],
+ "推理内容摘要": resp["推理内容"][:200] + "..." if len(resp["推理内容"]) > 200 else resp["推理内容"],
+ }
+ for resp in results_without_tools["响应结果"]
+ ],
+ "使用工具调用": {
+ k: (v if k != "所有耗时" else [float(f"{t:.2f}") for t in v])
+ for k, v in results_with_tools.items()
+ if k != "响应结果"
+ },
+ "使用工具调用_详细响应": [
+ {
+ "内容摘要": resp["内容"][:200] + "..." if len(resp["内容"]) > 200 else resp["内容"],
+ "推理内容摘要": resp["推理内容"][:200] + "..." if len(resp["推理内容"]) > 200 else resp["推理内容"],
+ "工具调用数量": len(resp["工具调用"]),
+ "工具调用详情": [
+ {"工具名称": tool["function"]["name"], "参数": tool["function"].get("arguments", {})}
+ for tool in resp["工具调用"]
+ ],
+ }
+ for resp in results_with_tools["响应结果"]
+ ],
+ "差异百分比": float(f"{diff_percent:.2f}"),
+ }
+
+ with open("llm_tool_benchmark_results.json", "w", encoding="utf-8") as f:
+ json.dump(results, f, ensure_ascii=False, indent=2)
+
+ print("\n测试结果已保存到 llm_tool_benchmark_results.json")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())