pull/1001/head
SnowindMe 2025-04-26 14:36:36 +08:00
commit 1b6c052079
73 changed files with 5199 additions and 3199 deletions

View File

@ -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

View File

@ -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
# 复制项目代码

View File

@ -14,7 +14,7 @@
<p align="center">
<a href="https://github.com/MaiM-with-u/MaiBot/">
<img src="depends-data/maimai.png" alt="Logo" style="max-width: 200px">
<img src="depends-data/maimai.png" alt="Logo" style="width: 200px">
</a>
<br />
<a href="https://space.bilibili.com/1344099355">

View File

@ -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"
) # 存储原始时间戳

Binary file not shown.

View File

@ -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"
)
# 默认项目目录

View File

@ -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")

View File

@ -0,0 +1 @@
pass

View File

@ -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": (
"<white>{time:YYYY-MM-DD HH:mm:ss}</white> | "
"<level>{level: <8}</level> | "
"<light-yellow>海马体</light-yellow> | "
"<level>{message}</level>"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 海马体 | {message}",
},
"simple": {
"console_format": (
"<level>{time:MM-DD HH:mm}</level> | <light-yellow>海马体</light-yellow> | <light-yellow>{message}</light-yellow>"
),
"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": (
"<white>{time:YYYY-MM-DD HH:mm:ss}</white> | "
"<level>{level: <8}</level> | "
"<light-green>心情</light-green> | "
"<magenta>心情</magenta> | "
"<level>{message}</level>"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 心情 | {message}",
},
"simple": {
"console_format": "<level>{time:MM-DD HH:mm}</level> | <magenta>心情</magenta> | {message}",
"console_format": "<level>{time:MM-DD HH:mm}</level> | <magenta>心情 | {message} </magenta>",
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 心情 | {message}",
},
}
@ -284,15 +315,13 @@ CHAT_STYLE_CONFIG = {
"console_format": (
"<white>{time:YYYY-MM-DD HH:mm:ss}</white> | "
"<level>{level: <8}</level> | "
"<light-blue>见闻</light-blue> | "
"<green>见闻</green> | "
"<level>{message}</level>"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 见闻 | {message}",
},
"simple": {
"console_format": (
"<level>{time:MM-DD HH:mm}</level> | <light-blue>见闻</light-blue> | <green>{message}</green>"
), # noqa: E501
"console_format": ("<level>{time:MM-DD HH:mm}</level> | <green>见闻</green> | <green>{message}</green>"), # 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": (
"<white>{time:YYYY-MM-DD HH:mm:ss}</white> | "
"<level>{level: <8}</level> | "
"<light-blue>麦麦水群</light-blue> | "
"<level>{message}</level>"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}",
},
"simple": {
"console_format": ("<level>{time:MM-DD HH:mm}</level> | <fg #3399FF>麦麦水群 | {message}</fg #3399FF>"), # 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": (
"<white>{time:YYYY-MM-DD HH:mm:ss}</white> | "
@ -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": (
"<level>{time:MM-DD HH:mm}</level> | <light-blue>麦麦小脑袋</light-blue> | <light-blue>{message}</light-blue>"
), # noqa: E501
"console_format": ("<level>{time:MM-DD HH:mm}</level> | <fg #66CCFF>麦麦小脑袋 | {message}</fg #66CCFF>"), # noqa: E501
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}",
},
}
SUBHEARTFLOW_MANAGER_STYLE_CONFIG = {
"advanced": {
"console_format": (
"<white>{time:YYYY-MM-DD HH:mm:ss}</white> | "
"<level>{level: <8}</level> | "
"<light-blue>麦麦水群[管理]</light-blue> | "
"<level>{message}</level>"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群[管理] | {message}",
},
"simple": {
"console_format": ("<level>{time:MM-DD HH:mm}</level> | <fg #005BA2>麦麦水群[管理] | {message}</fg #005BA2>"), # 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": (
"<white>{time:YYYY-MM-DD HH:mm:ss}</white> | "
"<level>{level: <8}</level> | "
"<light-blue>聊天流</light-blue> | "
"<level>{message}</level>"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 聊天流 | {message}",
},
"simple": {
"console_format": (
"<level>{time:MM-DD HH:mm}</level> | <light-blue>聊天流</light-blue> | <light-blue>{message}</light-blue>"
),
"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": (
"<white>{time:YYYY-MM-DD HH:mm:ss}</white> | "
"<level>{level: <8}</level> | "
"<light-blue>小脑袋管理</light-blue> | "
"<level>{message}</level>"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 小脑袋管理 | {message}",
},
"simple": {
"console_format": (
"<level>{time:MM-DD HH:mm}</level> | <light-blue>小脑袋管理</light-blue> | <light-blue>{message}</light-blue>"
), # 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": (
"<white>{time:YYYY-MM-DD HH:mm:ss}</white> | "
"<level>{level: <8}</level> | "
"<light-blue>PFC私聊规划</light-blue> | "
"<level>{message}</level>"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | PFC私聊规划 | {message}",
},
"simple": {
"console_format": "<level>{time:MM-DD HH:mm}</level> | <light-blue>PFC私聊规划 | {message} </light-blue>", # 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": (
"<white>{time:YYYY-MM-DD HH:mm:ss}</white> | "
"<level>{level: <8}</level> | "
"<light-blue>表情</light-blue> | "
"<fg #FFD700>表情包</fg #FFD700> | "
"<level>{message}</level>"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 表情 | {message}",
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 表情 | {message}",
},
"simple": {
"console_format": "<level>{time:MM-DD HH:mm}</level> | <light-blue>表情 | {message} </light-blue>", # noqa: E501
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 表情 | {message}",
"console_format": "<level>{time:MM-DD HH:mm}</level> | <fg #FFD700>表情包 | {message} </fg #FFD700>", # 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": "<level>{time:MM-DD HH:mm}</level> | <light-blue>麦麦状态 | {message} </light-blue>", # noqa: E501
"console_format": "<level>{time:MM-DD HH:mm}</level> | <fg #66CCFF>麦麦状态 | {message} </fg #66CCFF>", # noqa: E501
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦状态 | {message}",
},
}
# 海马体日志样式配置
MEMORY_STYLE_CONFIG = {
"advanced": {
"console_format": (
"<white>{time:YYYY-MM-DD HH:mm:ss}</white> | "
"<level>{level: <8}</level> | "
"<light-yellow>海马体</light-yellow> | "
"<level>{message}</level>"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 海马体 | {message}",
},
"simple": {
"console_format": (
"<level>{time:MM-DD HH:mm}</level> | <fg #7CFFE6>海马体</fg #7CFFE6> | <fg #7CFFE6>{message}</fg #7CFFE6>"
),
"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": (
"<level>{time:MM-DD HH:mm}</level> | <light-green>LPMM</light-green> | <light-green>{message}</light-green>"
"<level>{time:MM-DD HH:mm}</level> | <fg #37FFB4>LPMM</fg #37FFB4> | <fg #37FFB4>{message}</fg #37FFB4>"
),
"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": (
"<white>{time:YYYY-MM-DD HH:mm:ss}</white> | "
"<level>{level: <8}</level> | "
"<fg #66CCFF>天依</fg #66CCFF> | "
"<level>{message}</level>"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 天依 | {message}",
},
"simple": {
"console_format": (
"<level>{time:MM-DD HH:mm}</level> | <fg #66CCFF>天依</fg #66CCFF> | <fg #66CCFF>{message}</fg #66CCFF>"
),
"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:

View File

@ -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"]

View File

@ -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: 工具执行结果

View File

@ -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:

View File

@ -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)
# 调用知识库搜索

View File

@ -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)
# 将主题字符串转换为列表

View File

@ -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:

View File

@ -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)
# 调用知识库搜索

View File

@ -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)
# 使用工具名称作为键

View File

@ -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个性信息且获取的聊天内容有问题
## 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` 代码确认)。
- `

View File

@ -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):
"""执行一轮状态日志记录。"""

View File

@ -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()

View File

@ -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` 队列来保存最近的循环记录,方便调试和分析。

View File

@ -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] 正在停止任务和子心流...")

View File

@ -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

View File

@ -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:

View File

@ -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):
"""

View File

@ -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}"

View File

@ -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()

View File

@ -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()

View File

@ -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}")

View File

@ -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():

View File

@ -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)}"

View File

@ -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}"

View File

@ -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 # 出错也要尝试恢复状态

View File

@ -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,

View File

@ -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
# # 创建一个内存消息存储实现,用于测试

View File

@ -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()

View File

@ -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)}")

View File

@ -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 "未找到相关知识", "无记忆匹配"

View File

@ -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)

View File

@ -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)

View File

@ -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("倾听等待中...") # 同上,可以考虑注释掉

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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()

View File

@ -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":

View File

@ -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:

View File

@ -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)}")

View File

@ -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}"
)

View File

@ -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

View File

@ -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")

View File

@ -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()

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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:

View File

@ -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()`
- 错误追踪:详细的异常记录
- 响应监控:完整的状态跟踪

View File

@ -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

View File

@ -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

View File

@ -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"),
)

View File

@ -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())

View File

@ -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,

View File

@ -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",
]

View File

@ -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())

View File

@ -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)

View File

@ -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"<think>.*?</think>", "", 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向量

View File

@ -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}"
)

View File

@ -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

View File

@ -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:

View File

@ -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)}"

View File

@ -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

View File

@ -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())