From d9a66ee7cb415efbab54651720f1fe94326c2b45 Mon Sep 17 00:00:00 2001 From: infinitycat Date: Wed, 30 Apr 2025 11:13:23 +0800 Subject: [PATCH 01/67] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9Elpmm=E7=9A=84Li?= =?UTF-8?q?nux=E5=BF=AB=E6=8D=B7=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/run_lpmm.sh | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 scripts/run_lpmm.sh diff --git a/scripts/run_lpmm.sh b/scripts/run_lpmm.sh new file mode 100644 index 00000000..05435365 --- /dev/null +++ b/scripts/run_lpmm.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +# Step 1: 自动定位项目根目录(即 scripts 目录的上级目录) +SCRIPTS_DIR="scripts" +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +PROJECT_ROOT=$(cd "$SCRIPT_DIR/.." && pwd) + +# Step 2: 检查 scripts 目录是否存在 +if [ ! -d "$PROJECT_ROOT/$SCRIPTS_DIR" ]; then + echo "❌ 错误:项目根目录中找不到 scripts 目录" >&2 + echo "当前路径: $SCRIPT_DIR" >&2 + exit 1 +fi + +# Step 3: 切换到项目根目录 +cd "$PROJECT_ROOT" || { + echo "❌ 无法切换到项目根目录: $PROJECT_ROOT" >&2 + exit 1 +} + +# Step 4: 运行每个 Python 脚本并检查退出状态 +echo "🔄 正在运行 text_pre_process.py" +python3 scripts/text_pre_process.py +if [ $? -ne 0 ]; then + echo "❌ text_pre_process.py 执行失败" >&2 + exit 1 +fi + +echo "🔄 正在运行 info_extraction.py" +python3 scripts/info_extraction.py +if [ $? -ne 0 ]; then + echo "❌ info_extraction.py 执行失败" >&2 + exit 1 +fi + +echo "🔄 正在运行 import_openie.py" +python3 scripts/import_openie.py +if [ $? -ne 0 ]; then + echo "❌ import_openie.py 执行失败" >&2 + exit 1 +fi + +echo "✅ 所有脚本执行完成" \ No newline at end of file From 2dac54c30692ba5dd3c5f74863a3614112bf1264 Mon Sep 17 00:00:00 2001 From: infinitycat Date: Wed, 30 Apr 2025 15:09:34 +0800 Subject: [PATCH 02/67] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E5=90=8D=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/run_lpmm.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/run_lpmm.sh b/scripts/run_lpmm.sh index 05435365..15df0d7b 100644 --- a/scripts/run_lpmm.sh +++ b/scripts/run_lpmm.sh @@ -19,10 +19,10 @@ cd "$PROJECT_ROOT" || { } # Step 4: 运行每个 Python 脚本并检查退出状态 -echo "🔄 正在运行 text_pre_process.py" -python3 scripts/text_pre_process.py +echo "🔄 正在运行 raw_data_preprocessor.py" +python3 scripts/raw_data_preprocessor.py if [ $? -ne 0 ]; then - echo "❌ text_pre_process.py 执行失败" >&2 + echo "❌ raw_data_preprocessor.py 执行失败" >&2 exit 1 fi From 6f41d39d7220a8152b0473abee3c06898e8686b2 Mon Sep 17 00:00:00 2001 From: infinitycat Date: Wed, 30 Apr 2025 16:09:00 +0800 Subject: [PATCH 03/67] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96lpmm=E8=84=9A?= =?UTF-8?q?=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/run_lpmm.sh | 51 ++++++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/scripts/run_lpmm.sh b/scripts/run_lpmm.sh index 15df0d7b..33eb7284 100644 --- a/scripts/run_lpmm.sh +++ b/scripts/run_lpmm.sh @@ -1,5 +1,9 @@ #!/bin/sh +# ============================================== +# 环境初始化:确保Python脚本在正确的目录下运行 +# ============================================== + # Step 1: 自动定位项目根目录(即 scripts 目录的上级目录) SCRIPTS_DIR="scripts" SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) @@ -8,36 +12,41 @@ PROJECT_ROOT=$(cd "$SCRIPT_DIR/.." && pwd) # Step 2: 检查 scripts 目录是否存在 if [ ! -d "$PROJECT_ROOT/$SCRIPTS_DIR" ]; then echo "❌ 错误:项目根目录中找不到 scripts 目录" >&2 - echo "当前路径: $SCRIPT_DIR" >&2 + echo "当前路径: $PROJECT_ROOT" >&2 exit 1 fi -# Step 3: 切换到项目根目录 +# Step 3: 设置Python运行环境 +export PYTHONPATH="$PROJECT_ROOT:$PYTHONPATH" # 将项目根目录加入Python路径 cd "$PROJECT_ROOT" || { echo "❌ 无法切换到项目根目录: $PROJECT_ROOT" >&2 exit 1 } -# Step 4: 运行每个 Python 脚本并检查退出状态 -echo "🔄 正在运行 raw_data_preprocessor.py" -python3 scripts/raw_data_preprocessor.py -if [ $? -ne 0 ]; then - echo "❌ raw_data_preprocessor.py 执行失败" >&2 - exit 1 -fi +# Step 4: 打印关键路径信息(调试用) +echo "============================" +echo "项目根目录: $PROJECT_ROOT" +echo "Python路径: $PYTHONPATH" +echo "当前工作目录: $(pwd)" +echo "============================" -echo "🔄 正在运行 info_extraction.py" -python3 scripts/info_extraction.py -if [ $? -ne 0 ]; then - echo "❌ info_extraction.py 执行失败" >&2 - exit 1 -fi +# ============================================== +# 执行Python脚本 +# ============================================== -echo "🔄 正在运行 import_openie.py" -python3 scripts/import_openie.py -if [ $? -ne 0 ]; then - echo "❌ import_openie.py 执行失败" >&2 - exit 1 -fi +run_python_script() { + local script_name=$1 + echo "🔄 正在运行 $script_name" + python3 "scripts/$script_name" + if [ $? -ne 0 ]; then + echo "❌ $script_name 执行失败" >&2 + exit 1 + fi +} + +# 按顺序运行脚本 +run_python_script "raw_data_preprocessor.py" +run_python_script "info_extraction.py" +run_python_script "import_openie.py" echo "✅ 所有脚本执行完成" \ No newline at end of file From 16f765d86b64934415d319c41c3751fa1a304efd Mon Sep 17 00:00:00 2001 From: infinitycat Date: Wed, 30 Apr 2025 16:10:16 +0800 Subject: [PATCH 04/67] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96lpmm=E8=84=9A?= =?UTF-8?q?=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/run_lpmm.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/run_lpmm.sh b/scripts/run_lpmm.sh index 33eb7284..1671f011 100644 --- a/scripts/run_lpmm.sh +++ b/scripts/run_lpmm.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # ============================================== # 环境初始化:确保Python脚本在正确的目录下运行 From 850db4b3e76e01c11a07bc6165207736afd91717 Mon Sep 17 00:00:00 2001 From: infinitycat Date: Wed, 30 Apr 2025 16:12:27 +0800 Subject: [PATCH 05/67] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96lpmm=E8=84=9A?= =?UTF-8?q?=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/run_lpmm.sh | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/scripts/run_lpmm.sh b/scripts/run_lpmm.sh index 1671f011..23436388 100644 --- a/scripts/run_lpmm.sh +++ b/scripts/run_lpmm.sh @@ -1,52 +1,51 @@ #!/bin/bash # ============================================== -# 环境初始化:确保Python脚本在正确的目录下运行 +# Environment Initialization # ============================================== -# Step 1: 自动定位项目根目录(即 scripts 目录的上级目录) +# Step 1: Locate project root directory SCRIPTS_DIR="scripts" SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) PROJECT_ROOT=$(cd "$SCRIPT_DIR/.." && pwd) -# Step 2: 检查 scripts 目录是否存在 +# Step 2: Verify scripts directory exists if [ ! -d "$PROJECT_ROOT/$SCRIPTS_DIR" ]; then - echo "❌ 错误:项目根目录中找不到 scripts 目录" >&2 - echo "当前路径: $PROJECT_ROOT" >&2 + echo "❌ Error: scripts directory not found in project root" >&2 + echo "Current path: $PROJECT_ROOT" >&2 exit 1 fi -# Step 3: 设置Python运行环境 -export PYTHONPATH="$PROJECT_ROOT:$PYTHONPATH" # 将项目根目录加入Python路径 +# Step 3: Set up Python environment +export PYTHONPATH="$PROJECT_ROOT:$PYTHONPATH" cd "$PROJECT_ROOT" || { - echo "❌ 无法切换到项目根目录: $PROJECT_ROOT" >&2 + echo "❌ Failed to cd to project root: $PROJECT_ROOT" >&2 exit 1 } -# Step 4: 打印关键路径信息(调试用) +# Debug info echo "============================" -echo "项目根目录: $PROJECT_ROOT" -echo "Python路径: $PYTHONPATH" -echo "当前工作目录: $(pwd)" +echo "Project Root: $PROJECT_ROOT" +echo "Python Path: $PYTHONPATH" +echo "Working Dir: $(pwd)" echo "============================" # ============================================== -# 执行Python脚本 +# Python Script Execution # ============================================== run_python_script() { local script_name=$1 - echo "🔄 正在运行 $script_name" - python3 "scripts/$script_name" - if [ $? -ne 0 ]; then - echo "❌ $script_name 执行失败" >&2 + echo "🔄 Running $script_name" + if ! python3 "scripts/$script_name"; then + echo "❌ $script_name failed" >&2 exit 1 fi } -# 按顺序运行脚本 +# Execute scripts in order run_python_script "raw_data_preprocessor.py" run_python_script "info_extraction.py" run_python_script "import_openie.py" -echo "✅ 所有脚本执行完成" \ No newline at end of file +echo "✅ All scripts completed successfully" \ No newline at end of file From 5141862cf326db44ff33a7699ca226c89b47303c Mon Sep 17 00:00:00 2001 From: infinitycat Date: Thu, 1 May 2025 01:18:00 +0800 Subject: [PATCH 06/67] =?UTF-8?q?fix:=20=E4=B8=80=E4=B8=AA=E5=B0=8F?= =?UTF-8?q?=E8=A1=A5=E4=B8=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/run_lpmm.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/run_lpmm.sh b/scripts/run_lpmm.sh index 23436388..f3f54610 100644 --- a/scripts/run_lpmm.sh +++ b/scripts/run_lpmm.sh @@ -37,7 +37,7 @@ echo "============================" run_python_script() { local script_name=$1 echo "🔄 Running $script_name" - if ! python3 "scripts/$script_name"; then + if ! python3 "$SCRIPTS_DIR/$script_name"; then echo "❌ $script_name failed" >&2 exit 1 fi From 2f669c705543ec4efb36ad6d5490790ada710964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E7=8C=AB?= Date: Thu, 1 May 2025 05:58:18 +0900 Subject: [PATCH 07/67] QA: Update requirements and refactor message handling logic etc. --- bot.py | 1 - requirements.txt | Bin 732 -> 762 bytes scripts/interest_monitor_gui.py | 39 +- src/common/logger.py | 12 +- src/common/message_repository.py | 19 +- src/heart_flow/background_tasks.py | 65 ++-- src/heart_flow/interest_logger.py | 13 +- src/heart_flow/sub_mind.py | 1 + src/plugins/PFC/chat_observer.py | 4 + src/plugins/PFC/message_storage.py | 4 +- src/plugins/PFC/observation_info.py | 4 + src/plugins/PFC/pfc.py | 6 +- src/plugins/chat/bot.py | 4 +- src/plugins/chat/message_sender.py | 72 ++-- src/plugins/chat/utils.py | 2 +- src/plugins/emoji_system/emoji_manager.py | 349 ++++++++---------- src/plugins/heartFC_chat/heartFC_chat.py | 39 +- src/plugins/heartFC_chat/heartFC_sender.py | 39 +- .../heartFC_chat/heartflow_processor.py | 254 ++++++------- .../heartFC_chat/heartflow_prompt_builder.py | 181 ++++----- src/plugins/knowledge/src/qa_manager.py | 2 +- .../respon_info_catcher/info_catcher.py | 24 +- src/plugins/schedule/schedule_generator.py | 1 + src/plugins/utils/chat_message_builder.py | 6 +- src/plugins/willing/mode_custom.py | 18 + 25 files changed, 578 insertions(+), 581 deletions(-) diff --git a/bot.py b/bot.py index d547c360..770f5365 100644 --- a/bot.py +++ b/bot.py @@ -119,7 +119,6 @@ async def graceful_shutdown(): for task in tasks: task.cancel() await asyncio.gather(*tasks, return_exceptions=True) - except Exception as e: logger.error(f"麦麦关闭失败: {e}") diff --git a/requirements.txt b/requirements.txt index 06068d888e7575b1d9352a71d9b73111ebaa78b2..91ae096c192cc55966cf12380ae1df85b176f345 100644 GIT binary patch delta 38 ocmcb^`iphL9VWQ~hD3&BhHN02&XCEF2PC-|ih!(Sh72GJ0NG9m>;M1& delta 7 OcmeyxdWUtx9VP$|B?E5& diff --git a/scripts/interest_monitor_gui.py b/scripts/interest_monitor_gui.py index 1f03b969..0c44507c 100644 --- a/scripts/interest_monitor_gui.py +++ b/scripts/interest_monitor_gui.py @@ -28,8 +28,26 @@ matplotlib.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei"] matplotlib.rcParams["axes.unicode_minus"] = False # 解决负号'-'显示为方块的问题 +def get_random_color(): + """生成随机颜色用于区分线条""" + return "#{:06x}".format(random.randint(0, 0xFFFFFF)) + + +def format_timestamp(ts): + """辅助函数:格式化时间戳,处理 None 或无效值""" + if ts is None: + return "N/A" + try: + # 假设 ts 是 float 类型的时间戳 + dt_object = datetime.fromtimestamp(float(ts)) + return dt_object.strftime("%Y-%m-%d %H:%M:%S") + except (ValueError, TypeError): + return "Invalid Time" + + class InterestMonitorApp: def __init__(self, root): + self._main_mind_loaded = None self.root = root self.root.title(WINDOW_TITLE) self.root.geometry("1800x800") # 调整窗口大小以适应图表 @@ -173,10 +191,6 @@ class InterestMonitorApp: """当 Combobox 选择改变时调用,更新单个流的图表""" self.update_single_stream_plot() - def get_random_color(self): - """生成随机颜色用于区分线条""" - return "#{:06x}".format(random.randint(0, 0xFFFFFF)) - def load_main_mind_history(self): """只读取包含main_mind的日志行,维护历史想法队列""" if not os.path.exists(LOG_FILE_PATH): @@ -332,7 +346,7 @@ class InterestMonitorApp: new_probability_history[stream_id] = deque(maxlen=MAX_HISTORY_POINTS) # 创建概率 deque # 检查是否已有颜色,没有则分配 if stream_id not in self.stream_colors: - self.stream_colors[stream_id] = self.get_random_color() + self.stream_colors[stream_id] = get_random_color() # *** 存储此 stream_id 最新的显示名称 *** new_stream_display_names[stream_id] = group_name @@ -593,17 +607,6 @@ class InterestMonitorApp: # --- 新增:重新绘制画布 --- self.canvas_single.draw() - def format_timestamp(self, ts): - """辅助函数:格式化时间戳,处理 None 或无效值""" - if ts is None: - return "N/A" - try: - # 假设 ts 是 float 类型的时间戳 - dt_object = datetime.fromtimestamp(float(ts)) - return dt_object.strftime("%Y-%m-%d %H:%M:%S") - except (ValueError, TypeError): - return "Invalid Time" - def update_single_stream_details(self, stream_id): """更新单个流详情区域的标签内容""" if stream_id: @@ -616,8 +619,8 @@ class InterestMonitorApp: self.single_stream_sub_mind.set(f"想法: {sub_mind}") self.single_stream_chat_state.set(f"状态: {chat_state}") self.single_stream_threshold.set(f"阈值以上: {'是' if threshold else '否'}") - self.single_stream_last_active.set(f"最后活跃: {self.format_timestamp(last_active_ts)}") - self.single_stream_last_interaction.set(f"最后交互: {self.format_timestamp(last_interaction_ts)}") + self.single_stream_last_active.set(f"最后活跃: {format_timestamp(last_active_ts)}") + self.single_stream_last_interaction.set(f"最后交互: {format_timestamp(last_interaction_ts)}") else: # 如果没有选择流,则清空详情 self.single_stream_sub_mind.set("想法: N/A") diff --git a/src/common/logger.py b/src/common/logger.py index 6c95935e..a0d621d9 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -321,7 +321,7 @@ CHAT_STYLE_CONFIG = { "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 见闻 | {message}", }, "simple": { - "console_format": ("{time:MM-DD HH:mm} | 见闻 | {message}"), # noqa: E501 + "console_format": "{time:MM-DD HH:mm} | 见闻 | {message}", # noqa: E501 "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 见闻 | {message}", }, } @@ -353,7 +353,7 @@ SUB_HEARTFLOW_STYLE_CONFIG = { "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}", }, "simple": { - "console_format": ("{time:MM-DD HH:mm} | 麦麦水群 | {message}"), # noqa: E501 + "console_format": "{time:MM-DD HH:mm} | 麦麦水群 | {message}", # noqa: E501 "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群 | {message}", }, } @@ -369,7 +369,7 @@ SUB_HEARTFLOW_MIND_STYLE_CONFIG = { "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}", }, "simple": { - "console_format": ("{time:MM-DD HH:mm} | 麦麦小脑袋 | {message}"), # noqa: E501 + "console_format": "{time:MM-DD HH:mm} | 麦麦小脑袋 | {message}", # noqa: E501 "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}", }, } @@ -385,7 +385,7 @@ SUBHEARTFLOW_MANAGER_STYLE_CONFIG = { "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群[管理] | {message}", }, "simple": { - "console_format": ("{time:MM-DD HH:mm} | 麦麦水群[管理] | {message}"), # noqa: E501 + "console_format": "{time:MM-DD HH:mm} | 麦麦水群[管理] | {message}", # noqa: E501 "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群[管理] | {message}", }, } @@ -633,7 +633,7 @@ HFC_STYLE_CONFIG = { "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注聊天 | {message}", }, "simple": { - "console_format": ("{time:MM-DD HH:mm} | 专注聊天 | {message}"), + "console_format": "{time:MM-DD HH:mm} | 专注聊天 | {message}", "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注聊天 | {message}", }, } @@ -1031,7 +1031,7 @@ def add_custom_style_handler( # retention=current_config["retention"], # compression=current_config["compression"], # encoding="utf-8", - # filter=lambda record: record["extra"].get("module") == module_name + # message_filter=lambda record: record["extra"].get("module") == module_name # and record["extra"].get("custom_style") == style_name, # enqueue=True, # ) diff --git a/src/common/message_repository.py b/src/common/message_repository.py index fc7b7e54..72643f91 100644 --- a/src/common/message_repository.py +++ b/src/common/message_repository.py @@ -7,13 +7,16 @@ logger = get_module_logger(__name__) def find_messages( - filter: Dict[str, Any], sort: Optional[List[tuple[str, int]]] = None, limit: int = 0, limit_mode: str = "latest" + message_filter: Dict[str, Any], + sort: Optional[List[tuple[str, int]]] = None, + limit: int = 0, + limit_mode: str = "latest", ) -> List[Dict[str, Any]]: """ 根据提供的过滤器、排序和限制条件查找消息。 Args: - filter: MongoDB 查询过滤器。 + message_filter: MongoDB 查询过滤器。 sort: MongoDB 排序条件列表,例如 [('time', 1)]。仅在 limit 为 0 时生效。 limit: 返回的最大文档数,0表示不限制。 limit_mode: 当 limit > 0 时生效。 'earliest' 表示获取最早的记录, 'latest' 表示获取最新的记录(结果仍按时间正序排列)。默认为 'latest'。 @@ -22,7 +25,7 @@ def find_messages( 消息文档列表,如果出错则返回空列表。 """ try: - query = db.messages.find(filter) + query = db.messages.find(message_filter) results: List[Dict[str, Any]] = [] if limit > 0: @@ -46,28 +49,28 @@ def find_messages( return results except Exception as e: log_message = ( - f"查找消息失败 (filter={filter}, sort={sort}, limit={limit}, limit_mode={limit_mode}): {e}\n" + f"查找消息失败 (filter={message_filter}, sort={sort}, limit={limit}, limit_mode={limit_mode}): {e}\n" + traceback.format_exc() ) logger.error(log_message) return [] -def count_messages(filter: Dict[str, Any]) -> int: +def count_messages(message_filter: Dict[str, Any]) -> int: """ 根据提供的过滤器计算消息数量。 Args: - filter: MongoDB 查询过滤器。 + message_filter: MongoDB 查询过滤器。 Returns: 符合条件的消息数量,如果出错则返回 0。 """ try: - count = db.messages.count_documents(filter) + count = db.messages.count_documents(message_filter) return count except Exception as e: - log_message = f"计数消息失败 (filter={filter}): {e}\n" + traceback.format_exc() + log_message = f"计数消息失败 (message_filter={message_filter}): {e}\n" + traceback.format_exc() logger.error(log_message) return 0 diff --git a/src/heart_flow/background_tasks.py b/src/heart_flow/background_tasks.py index 56fee2a9..38360653 100644 --- a/src/heart_flow/background_tasks.py +++ b/src/heart_flow/background_tasks.py @@ -25,6 +25,33 @@ STATE_UPDATE_INTERVAL_SECONDS = 60 LOG_INTERVAL_SECONDS = 3 +async def _run_periodic_loop( + task_name: str, interval: int, task_func: Callable[..., Coroutine[Any, Any, None]], **kwargs +): + """周期性任务主循环""" + while True: + start_time = asyncio.get_event_loop().time() + # logger.debug(f"开始执行后台任务: {task_name}") + + try: + await task_func(**kwargs) # 执行实际任务 + except asyncio.CancelledError: + logger.info(f"任务 {task_name} 已取消") + break + except Exception as e: + logger.error(f"任务 {task_name} 执行出错: {e}") + logger.error(traceback.format_exc()) + + # 计算并执行间隔等待 + elapsed = asyncio.get_event_loop().time() - start_time + sleep_time = max(0, interval - elapsed) + # if sleep_time < 0.1: # 任务超时处理, DEBUG 时可能干扰断点 + # logger.warning(f"任务 {task_name} 超时执行 ({elapsed:.2f}s > {interval}s)") + await asyncio.sleep(sleep_time) + + logger.debug(f"任务循环结束: {task_name}") # 调整日志信息 + + class BackgroundTaskManager: """管理 Heartflow 的后台周期性任务。""" @@ -143,32 +170,6 @@ class BackgroundTaskManager: # 第三步:清空任务列表 self._tasks = [] # 重置任务列表 - async def _run_periodic_loop( - self, task_name: str, interval: int, task_func: Callable[..., Coroutine[Any, Any, None]], **kwargs - ): - """周期性任务主循环""" - while True: - start_time = asyncio.get_event_loop().time() - # logger.debug(f"开始执行后台任务: {task_name}") - - try: - await task_func(**kwargs) # 执行实际任务 - except asyncio.CancelledError: - logger.info(f"任务 {task_name} 已取消") - break - except Exception as e: - logger.error(f"任务 {task_name} 执行出错: {e}") - logger.error(traceback.format_exc()) - - # 计算并执行间隔等待 - elapsed = asyncio.get_event_loop().time() - start_time - sleep_time = max(0, interval - elapsed) - # if sleep_time < 0.1: # 任务超时处理, DEBUG 时可能干扰断点 - # logger.warning(f"任务 {task_name} 超时执行 ({elapsed:.2f}s > {interval}s)") - await asyncio.sleep(sleep_time) - - logger.debug(f"任务循环结束: {task_name}") # 调整日志信息 - async def _perform_state_update_work(self): """执行状态更新工作""" previous_status = self.mai_state_info.get_current_state() @@ -249,33 +250,33 @@ class BackgroundTaskManager: # --- Specific Task Runners --- # async def _run_state_update_cycle(self, interval: int): - await self._run_periodic_loop( + await _run_periodic_loop( task_name="State Update", interval=interval, task_func=self._perform_state_update_work ) async def _run_absent_into_chat(self, interval: int): - await self._run_periodic_loop( + await _run_periodic_loop( task_name="Into Chat", interval=interval, task_func=self._perform_absent_into_chat ) async def _run_normal_chat_timeout_check_cycle(self, interval: int): - await self._run_periodic_loop( + await _run_periodic_loop( task_name="Normal Chat Timeout Check", interval=interval, task_func=self._normal_chat_timeout_check_work ) async def _run_cleanup_cycle(self): - await self._run_periodic_loop( + await _run_periodic_loop( task_name="Subflow Cleanup", interval=CLEANUP_INTERVAL_SECONDS, task_func=self._perform_cleanup_work ) async def _run_logging_cycle(self): - await self._run_periodic_loop( + await _run_periodic_loop( task_name="State Logging", interval=LOG_INTERVAL_SECONDS, task_func=self._perform_logging_work ) # --- 新增兴趣评估任务运行器 --- async def _run_into_focus_cycle(self): - await self._run_periodic_loop( + await _run_periodic_loop( task_name="Into Focus", interval=INTEREST_EVAL_INTERVAL_SECONDS, task_func=self._perform_into_focus_work, diff --git a/src/heart_flow/interest_logger.py b/src/heart_flow/interest_logger.py index 04cdb6f4..06d3f1cb 100644 --- a/src/heart_flow/interest_logger.py +++ b/src/heart_flow/interest_logger.py @@ -23,6 +23,12 @@ LOG_DIRECTORY = "logs/interest" HISTORY_LOG_FILENAME = "interest_history.log" +def _ensure_log_directory(): + """确保日志目录存在。""" + os.makedirs(LOG_DIRECTORY, exist_ok=True) + logger.info(f"已确保日志目录 '{LOG_DIRECTORY}' 存在") + + class InterestLogger: """负责定期记录主心流和所有子心流的状态到日志文件。""" @@ -37,12 +43,7 @@ class InterestLogger: self.subheartflow_manager = subheartflow_manager self.heartflow = heartflow # 存储 Heartflow 实例 self._history_log_file_path = os.path.join(LOG_DIRECTORY, HISTORY_LOG_FILENAME) - self._ensure_log_directory() - - def _ensure_log_directory(self): - """确保日志目录存在。""" - os.makedirs(LOG_DIRECTORY, exist_ok=True) - logger.info(f"已确保日志目录 '{LOG_DIRECTORY}' 存在") + _ensure_log_directory() async def get_all_subflow_states(self) -> Dict[str, Dict]: """并发获取所有活跃子心流的当前完整状态。""" diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index fbf1be87..f1716d24 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -86,6 +86,7 @@ def calculate_replacement_probability(similarity: float) -> float: class SubMind: def __init__(self, subheartflow_id: str, chat_state: ChatStateInfo, observations: Observation): + self.last_active_time = None self.subheartflow_id = subheartflow_id self.llm_model = LLMRequest( diff --git a/src/plugins/PFC/chat_observer.py b/src/plugins/PFC/chat_observer.py index 102c9502..2822e111 100644 --- a/src/plugins/PFC/chat_observer.py +++ b/src/plugins/PFC/chat_observer.py @@ -37,6 +37,10 @@ class ChatObserver: Args: stream_id: 聊天流ID """ + self.last_check_time = None + self.last_check_time = None + self.last_bot_speak_time = None + self.last_user_speak_time = None if stream_id in self._instances: raise RuntimeError(f"ChatObserver for {stream_id} already exists. Use get_instance() instead.") diff --git a/src/plugins/PFC/message_storage.py b/src/plugins/PFC/message_storage.py index b57f5d2b..cd6a01e3 100644 --- a/src/plugins/PFC/message_storage.py +++ b/src/plugins/PFC/message_storage.py @@ -51,11 +51,9 @@ class MongoDBMessageStorage(MessageStorage): """MongoDB消息存储实现""" async def get_messages_after(self, chat_id: str, message_time: float) -> List[Dict[str, Any]]: - query = {"chat_id": chat_id} + query = {"chat_id": chat_id, "time": {"$gt": message_time}} # print(f"storage_check_message: {message_time}") - query["time"] = {"$gt": message_time} - return list(db.messages.find(query).sort("time", 1)) async def get_messages_before(self, chat_id: str, time_point: float, limit: int = 5) -> List[Dict[str, Any]]: diff --git a/src/plugins/PFC/observation_info.py b/src/plugins/PFC/observation_info.py index 35f39301..af7f537b 100644 --- a/src/plugins/PFC/observation_info.py +++ b/src/plugins/PFC/observation_info.py @@ -158,6 +158,10 @@ class ObservationInfo: # meta_plan_trigger: bool = False # --- 修改:移除 __post_init__ 的参数 --- + def __init__(self): + self.chat_observer = None + self.chat_observer = None + def __post_init__(self): """初始化后创建handler并进行必要的设置""" self.chat_observer: Optional[ChatObserver] = None # 添加类型提示 diff --git a/src/plugins/PFC/pfc.py b/src/plugins/PFC/pfc.py index d6f4c519..e12cb242 100644 --- a/src/plugins/PFC/pfc.py +++ b/src/plugins/PFC/pfc.py @@ -147,14 +147,14 @@ 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: # 单个目标的情况 conversation_info.goal_list.append(result) - return (goal, "", reasoning) + return goal, "", reasoning # 如果解析失败,返回默认值 - return ("", "", "") + return "", "", "" async def _update_goals(self, new_goal: str, method: str, reasoning: str): """更新目标列表 diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 8051d0a8..7ca1483c 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -1,3 +1,5 @@ +from typing import Dict, Any + from ..moods.moods import MoodManager # 导入情绪管理器 from ...config.config import global_config from .message import MessageRecv @@ -46,7 +48,7 @@ class ChatBot: except Exception as e: logger.error(f"创建PFC聊天失败: {e}") - async def message_process(self, message_data: str) -> None: + async def message_process(self, message_data: Dict[str, Any]) -> None: """处理转化后的统一格式消息 这个函数本质是预处理一些数据,根据配置信息和消息内容,预处理消息,并分发到合适的消息处理器中 heart_flow模式:使用思维流系统进行回复 diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index 493397bb..30d943d9 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -17,6 +17,40 @@ from src.common.logger_manager import get_logger logger = get_logger("sender") +async def send_via_ws(message: MessageSending) -> None: + """通过 WebSocket 发送消息""" + try: + await send_message(message) + except Exception as e: + logger.error(f"WS发送失败: {e}") + raise ValueError(f"未找到平台:{message.message_info.platform} 的url配置,请检查配置文件") from e + + +async def send_message( + message: MessageSending, +) -> None: + """发送消息(核心发送逻辑)""" + + # --- 添加计算打字和延迟的逻辑 (从 heartflow_message_sender 移动并调整) --- + typing_time = calculate_typing_time( + input_string=message.processed_plain_text, + thinking_start_time=message.thinking_start_time, + is_emoji=message.is_emoji, + ) + # logger.trace(f"{message.processed_plain_text},{typing_time},计算输入时间结束") # 减少日志 + await asyncio.sleep(typing_time) + # logger.trace(f"{message.processed_plain_text},{typing_time},等待输入时间结束") # 减少日志 + # --- 结束打字延迟 --- + + message_preview = truncate_message(message.processed_plain_text) + + try: + await send_via_ws(message) + logger.success(f"发送消息 '{message_preview}' 成功") # 调整日志格式 + except Exception as e: + logger.error(f"发送消息 '{message_preview}' 失败: {str(e)}") + + class MessageSender: """发送器 (不再是单例)""" @@ -29,39 +63,6 @@ class MessageSender: """设置当前bot实例""" pass - async def send_via_ws(self, message: MessageSending) -> None: - """通过 WebSocket 发送消息""" - try: - await global_api.send_message(message) - except Exception as e: - logger.error(f"WS发送失败: {e}") - raise ValueError(f"未找到平台:{message.message_info.platform} 的url配置,请检查配置文件") from e - - async def send_message( - self, - message: MessageSending, - ) -> None: - """发送消息(核心发送逻辑)""" - - # --- 添加计算打字和延迟的逻辑 (从 heartflow_message_sender 移动并调整) --- - typing_time = calculate_typing_time( - input_string=message.processed_plain_text, - thinking_start_time=message.thinking_start_time, - is_emoji=message.is_emoji, - ) - # logger.trace(f"{message.processed_plain_text},{typing_time},计算输入时间结束") # 减少日志 - await asyncio.sleep(typing_time) - # logger.trace(f"{message.processed_plain_text},{typing_time},等待输入时间结束") # 减少日志 - # --- 结束打字延迟 --- - - message_preview = truncate_message(message.processed_plain_text) - - try: - await self.send_via_ws(message) - logger.success(f"发送消息 '{message_preview}' 成功") # 调整日志格式 - except Exception as e: - logger.error(f"发送消息 '{message_preview}' 失败: {str(e)}") - class MessageContainer: """单个聊天流的发送/思考消息容器""" @@ -119,7 +120,7 @@ class MessageContainer: """移除指定的消息对象,如果消息存在则返回True,否则返回False""" try: _initial_len = len(self.messages) - # 使用列表推导式或 filter 创建新列表,排除要删除的元素 + # 使用列表推导式或 message_filter 创建新列表,排除要删除的元素 # self.messages = [msg for msg in self.messages if msg is not message_to_remove] # 或者直接 remove (如果确定对象唯一性) if message_to_remove in self.messages: @@ -146,6 +147,7 @@ class MessageManager: """管理所有聊天流的消息容器 (不再是单例)""" def __init__(self): + self._processor_task = None self.containers: Dict[str, MessageContainer] = {} self.storage = MessageStorage() # 添加 storage 实例 self._running = True # 处理器运行状态 @@ -226,7 +228,7 @@ class MessageManager: await message.process() # 预处理消息内容 # 使用全局 message_sender 实例 - await message_sender.send_message(message) + await send_message(message) await self.storage.store_message(message, message.chat_stream) # 移除消息要在发送 *之后* diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index 71980f48..16581f3a 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -263,7 +263,7 @@ def split_into_sentences_w_remove_punctuation(text: str) -> List[str]: if char in separators: # 检查分割条件:如果分隔符左右都是英文字母,则不分割 can_split = True - if i > 0 and i < len(text) - 1: + if 0 < i < len(text) - 1: prev_char = text[i - 1] next_char = text[i + 1] # if is_english_letter(prev_char) and is_english_letter(next_char) and char == ' ': # 原计划只对空格应用此规则,现应用于所有分隔符 diff --git a/src/plugins/emoji_system/emoji_manager.py b/src/plugins/emoji_system/emoji_manager.py index d6da4ce3..a75a4f90 100644 --- a/src/plugins/emoji_system/emoji_manager.py +++ b/src/plugins/emoji_system/emoji_manager.py @@ -16,7 +16,6 @@ from ..chat.utils_image import image_path_to_base64, image_manager from ..models.utils_model import LLMRequest from src.common.logger_manager import get_logger - logger = get_logger("emoji") BASE_DIR = os.path.join("data") @@ -24,7 +23,6 @@ EMOJI_DIR = os.path.join(BASE_DIR, "emoji") # 表情包存储目录 EMOJI_REGISTED_DIR = os.path.join(BASE_DIR, "emoji_registed") # 已注册的表情包注册目录 MAX_EMOJI_FOR_PROMPT = 20 # 最大允许的表情包描述数量于图片替换的 prompt 中 - """ 还没经过测试,有些地方数据库和内存数据同步可能不完全 @@ -225,6 +223,140 @@ class MaiEmoji: return False +def _emoji_objects_to_readable_list(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 + + +def _to_emoji_objects(data): + emoji_objects = [] + load_errors = 0 + emoji_data_list = list(data) + + for emoji_data in emoji_data_list: + full_path = emoji_data.get("full_path") + if not full_path: + logger.warning(f"[加载错误] 数据库记录缺少 'full_path' 字段: {emoji_data.get('_id')}") + load_errors += 1 + continue # 跳过缺少 full_path 的记录 + + try: + # 使用 full_path 初始化 MaiEmoji 对象 + emoji = MaiEmoji(full_path=full_path) + + # 设置从数据库加载的属性 + emoji.hash = emoji_data.get("hash", "") + # 如果 hash 为空,也跳过?取决于业务逻辑 + if not emoji.hash: + logger.warning(f"[加载错误] 数据库记录缺少 'hash' 字段: {full_path}") + load_errors += 1 + continue + + emoji.description = emoji_data.get("description", "") + emoji.emotion = emoji_data.get("emotion", []) + emoji.usage_count = emoji_data.get("usage_count", 0) + # 优先使用 last_used_time,否则用 timestamp,最后用当前时间 + last_used = emoji_data.get("last_used_time") + timestamp = emoji_data.get("timestamp") + emoji.last_used_time = ( + last_used if last_used is not None else (timestamp if timestamp is not None else time.time()) + ) + emoji.register_time = timestamp if timestamp is not None else time.time() + emoji.format = emoji_data.get("format", "") # 加载格式 + + # 不需要再手动设置 path 和 filename,__init__ 会自动处理 + + emoji_objects.append(emoji) + + except ValueError as ve: # 捕获 __init__ 可能的错误 + logger.error(f"[加载错误] 初始化 MaiEmoji 失败 ({full_path}): {ve}") + load_errors += 1 + except Exception as e: + logger.error(f"[加载错误] 处理数据库记录时出错 ({full_path}): {str(e)}") + load_errors += 1 + return emoji_objects, load_errors + return emoji_objects, load_errors + + +def _ensure_emoji_dir(): + """确保表情存储目录存在""" + os.makedirs(EMOJI_DIR, exist_ok=True) + os.makedirs(EMOJI_REGISTED_DIR, exist_ok=True) + + +async def clear_temp_emoji(): + """清理临时表情包 + 清理/data/emoji和/data/image目录下的所有文件 + 当目录中文件数超过100时,会全部删除 + """ + + logger.info("[清理] 开始清理缓存...") + + for need_clear in (os.path.join(BASE_DIR, "emoji"), os.path.join(BASE_DIR, "image")): + if os.path.exists(need_clear): + files = os.listdir(need_clear) + # 如果文件数超过50就全部删除 + if len(files) > 100: + for filename in files: + file_path = os.path.join(need_clear, filename) + if os.path.isfile(file_path): + os.remove(file_path) + logger.debug(f"[清理] 删除: {filename}") + + logger.success("[清理] 完成") + + +async def clean_unused_emojis(emoji_dir, emoji_objects): + """清理指定目录中未被 emoji_objects 追踪的表情包文件""" + if not os.path.exists(emoji_dir): + logger.warning(f"[清理] 目标目录不存在,跳过清理: {emoji_dir}") + return + + try: + # 获取内存中所有有效表情包的完整路径集合 + tracked_full_paths = {emoji.full_path for emoji in emoji_objects if not emoji.is_deleted} + cleaned_count = 0 + + # 遍历指定目录中的所有文件 + for file_name in os.listdir(emoji_dir): + file_full_path = os.path.join(emoji_dir, file_name) + + # 确保处理的是文件而不是子目录 + if not os.path.isfile(file_full_path): + continue + + # 如果文件不在被追踪的集合中,则删除 + if file_full_path not in tracked_full_paths: + try: + os.remove(file_full_path) + logger.info(f"[清理] 删除未追踪的表情包文件: {file_full_path}") + cleaned_count += 1 + except Exception as e: + logger.error(f"[错误] 删除文件时出错 ({file_full_path}): {str(e)}") + + if cleaned_count > 0: + logger.success(f"[清理] 在目录 {emoji_dir} 中清理了 {cleaned_count} 个破损表情包。") + else: + logger.info(f"[清理] 目录 {emoji_dir} 中没有需要清理的。") + + except Exception as e: + logger.error(f"[错误] 清理未使用表情包文件时出错 ({emoji_dir}): {str(e)}") + + class EmojiManager: _instance = None @@ -235,6 +367,7 @@ class EmojiManager: return cls._instance def __init__(self): + self._initialized = None 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( @@ -248,23 +381,18 @@ class EmojiManager: logger.info("启动表情包管理器") - def _ensure_emoji_dir(self): - """确保表情存储目录存在""" - os.makedirs(EMOJI_DIR, exist_ok=True) - os.makedirs(EMOJI_REGISTED_DIR, exist_ok=True) - def initialize(self): """初始化数据库连接和表情目录""" if not self._initialized: try: self._ensure_emoji_collection() - self._ensure_emoji_dir() + _ensure_emoji_dir() self._initialized = True # 更新表情包数量 # 启动时执行一次完整性检查 # await self.check_emoji_file_integrity() - except Exception: - logger.exception("初始化表情管理器失败") + except Exception as e: + logger.exception(f"初始化表情管理器失败: {e}") def _ensure_db(self): """确保数据库已初始化""" @@ -291,12 +419,12 @@ class EmojiManager: db.emoji.create_index([("embedding", "2dsphere")]) db.emoji.create_index([("filename", 1)], unique=True) - def record_usage(self, hash: str): + def record_usage(self, emoji_hash: str): """记录表情使用次数""" try: - db.emoji.update_one({"hash": hash}, {"$inc": {"usage_count": 1}}) + db.emoji.update_one({"hash": emoji_hash}, {"$inc": {"usage_count": 1}}) for emoji in self.emoji_objects: - if emoji.hash == hash: + if emoji.hash == emoji_hash: emoji.usage_count += 1 break @@ -458,7 +586,7 @@ class EmojiManager: self.emoji_objects = [e for e in self.emoji_objects if e not in objects_to_remove] # 清理 EMOJI_REGISTED_DIR 目录中未被追踪的文件 - await self.clean_unused_emojis(EMOJI_REGISTED_DIR, self.emoji_objects) + await clean_unused_emojis(EMOJI_REGISTED_DIR, self.emoji_objects) # 输出清理结果 if removed_count > 0: @@ -477,7 +605,7 @@ class EmojiManager: while True: logger.info("[扫描] 开始检查表情包完整性...") await self.check_emoji_file_integrity() - await self.clear_temp_emoji() + await clear_temp_emoji() logger.info("[扫描] 开始扫描新表情包...") # 检查表情包目录是否存在 @@ -531,51 +659,7 @@ class EmojiManager: self._ensure_db() logger.info("[数据库] 开始加载所有表情包记录...") - all_emoji_data = list(db.emoji.find()) - emoji_objects = [] - load_errors = 0 - - for emoji_data in all_emoji_data: - full_path = emoji_data.get("full_path") - if not full_path: - logger.warning(f"[加载错误] 数据库记录缺少 'full_path' 字段: {emoji_data.get('_id')}") - load_errors += 1 - continue # 跳过缺少 full_path 的记录 - - try: - # 使用 full_path 初始化 MaiEmoji 对象 - emoji = MaiEmoji(full_path=full_path) - - # 设置从数据库加载的属性 - emoji.hash = emoji_data.get("hash", "") - # 如果 hash 为空,也跳过?取决于业务逻辑 - if not emoji.hash: - logger.warning(f"[加载错误] 数据库记录缺少 'hash' 字段: {full_path}") - load_errors += 1 - continue - - emoji.description = emoji_data.get("description", "") - emoji.emotion = emoji_data.get("emotion", []) - emoji.usage_count = emoji_data.get("usage_count", 0) - # 优先使用 last_used_time,否则用 timestamp,最后用当前时间 - last_used = emoji_data.get("last_used_time") - timestamp = emoji_data.get("timestamp") - emoji.last_used_time = ( - last_used if last_used is not None else (timestamp if timestamp is not None else time.time()) - ) - emoji.register_time = timestamp if timestamp is not None else time.time() - emoji.format = emoji_data.get("format", "") # 加载格式 - - # 不需要再手动设置 path 和 filename,__init__ 会自动处理 - - emoji_objects.append(emoji) - - except ValueError as ve: # 捕获 __init__ 可能的错误 - logger.error(f"[加载错误] 初始化 MaiEmoji 失败 ({full_path}): {ve}") - load_errors += 1 - except Exception as e: - logger.error(f"[加载错误] 处理数据库记录时出错 ({full_path}): {str(e)}") - load_errors += 1 + emoji_objects, load_errors = _to_emoji_objects(db.emoji.find()) # 更新内存中的列表和数量 self.emoji_objects = emoji_objects @@ -590,11 +674,11 @@ class EmojiManager: self.emoji_objects = [] # 加载失败则清空列表 self.emoji_num = 0 - async def get_emoji_from_db(self, hash=None): + async def get_emoji_from_db(self, emoji_hash=None): """获取指定哈希值的表情包并初始化为MaiEmoji类对象列表 (主要用于调试或特定查找) 参数: - hash: 可选,如果提供则只返回指定哈希值的表情包 + emoji_hash: 可选,如果提供则只返回指定哈希值的表情包 返回: list[MaiEmoji]: 表情包对象列表 @@ -603,49 +687,14 @@ class EmojiManager: self._ensure_db() query = {} - if hash: - query = {"hash": hash} + if emoji_hash: + query = {"hash": emoji_hash} else: logger.warning( "[查询] 未提供 hash,将尝试加载所有表情包,建议使用 get_all_emoji_from_db 更新管理器状态。" ) - emoji_data_list = list(db.emoji.find(query)) - emoji_objects = [] - load_errors = 0 - - for emoji_data in emoji_data_list: - full_path = emoji_data.get("full_path") - if not full_path: - logger.warning(f"[加载错误] 数据库记录缺少 'full_path' 字段: {emoji_data.get('_id')}") - load_errors += 1 - continue - - try: - emoji = MaiEmoji(full_path=full_path) - emoji.hash = emoji_data.get("hash", "") - if not emoji.hash: - logger.warning(f"[加载错误] 数据库记录缺少 'hash' 字段: {full_path}") - load_errors += 1 - continue - - emoji.description = emoji_data.get("description", "") - emoji.emotion = emoji_data.get("emotion", []) - emoji.usage_count = emoji_data.get("usage_count", 0) - last_used = emoji_data.get("last_used_time") - timestamp = emoji_data.get("timestamp") - emoji.last_used_time = ( - last_used if last_used is not None else (timestamp if timestamp is not None else time.time()) - ) - emoji.register_time = timestamp if timestamp is not None else time.time() - emoji.format = emoji_data.get("format", "") - emoji_objects.append(emoji) - except ValueError as ve: - logger.error(f"[加载错误] 初始化 MaiEmoji 失败 ({full_path}): {ve}") - load_errors += 1 - except Exception as e: - logger.error(f"[加载错误] 处理数据库记录时出错 ({full_path}): {str(e)}") - load_errors += 1 + emoji_objects, load_errors = _to_emoji_objects(db.emoji.find(query)) if load_errors > 0: logger.warning(f"[查询] 加载过程中出现 {load_errors} 个错误。") @@ -656,17 +705,17 @@ class EmojiManager: logger.error(f"[错误] 从数据库获取表情包对象失败: {str(e)}") return [] - async def get_emoji_from_manager(self, hash) -> Optional[MaiEmoji]: + async def get_emoji_from_manager(self, emoji_hash) -> Optional[MaiEmoji]: """从内存中的 emoji_objects 列表获取表情包 参数: - hash: 要查找的表情包哈希值 + emoji_hash: 要查找的表情包哈希值 返回: MaiEmoji 或 None: 如果找到则返回 MaiEmoji 对象,否则返回 None """ for emoji in self.emoji_objects: # 确保对象未被标记为删除且哈希值匹配 - if not emoji.is_deleted and emoji.hash == hash: + if not emoji.is_deleted and emoji.hash == emoji_hash: return emoji return None # 如果循环结束还没找到,则返回 None @@ -709,26 +758,6 @@ class EmojiManager: 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): """替换一个表情包 @@ -755,7 +784,7 @@ class EmojiManager: ) # 将表情包信息转换为可读的字符串 - emoji_info_list = self._emoji_objects_to_readable_list(selected_emojis) + emoji_info_list = _emoji_objects_to_readable_list(selected_emojis) # 构建提示词 prompt = ( @@ -853,7 +882,7 @@ class EmojiManager: ''' content, _ = await self.vlm.generate_response_for_image(prompt, image_base64, image_format) if content == "否": - return None, [] + return "", [] # 分析情感含义 emotion_prompt = f""" @@ -989,76 +1018,6 @@ class EmojiManager: logger.error(f"[错误] 删除异常处理文件时出错: {remove_error}") return False - async def clear_temp_emoji(self): - """清理临时表情包 - 清理/data/emoji和/data/image目录下的所有文件 - 当目录中文件数超过100时,会全部删除 - """ - - 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) > 100: - 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) > 100: - 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("[清理] 完成") - - async def clean_unused_emojis(self, emoji_dir, emoji_objects): - """清理指定目录中未被 emoji_objects 追踪的表情包文件""" - if not os.path.exists(emoji_dir): - logger.warning(f"[清理] 目标目录不存在,跳过清理: {emoji_dir}") - return - - try: - # 获取内存中所有有效表情包的完整路径集合 - tracked_full_paths = {emoji.full_path for emoji in emoji_objects if not emoji.is_deleted} - cleaned_count = 0 - - # 遍历指定目录中的所有文件 - for file_name in os.listdir(emoji_dir): - file_full_path = os.path.join(emoji_dir, file_name) - - # 确保处理的是文件而不是子目录 - if not os.path.isfile(file_full_path): - continue - - # 如果文件不在被追踪的集合中,则删除 - if file_full_path not in tracked_full_paths: - try: - os.remove(file_full_path) - logger.info(f"[清理] 删除未追踪的表情包文件: {file_full_path}") - cleaned_count += 1 - except Exception as e: - logger.error(f"[错误] 删除文件时出错 ({file_full_path}): {str(e)}") - - if cleaned_count > 0: - logger.success(f"[清理] 在目录 {emoji_dir} 中清理了 {cleaned_count} 个破损表情包。") - else: - logger.info(f"[清理] 目录 {emoji_dir} 中没有需要清理的。") - - except Exception as e: - logger.error(f"[错误] 清理未使用表情包文件时出错 ({emoji_dir}): {str(e)}") - # 创建全局单例 emoji_manager = EmojiManager() diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 73d679e4..47d420dd 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -144,6 +144,25 @@ class SenderError(HeartFCError): pass +async def _handle_cycle_delay(action_taken_this_cycle: bool, cycle_start_time: float, log_prefix: str): + """处理循环延迟""" + cycle_duration = time.monotonic() - cycle_start_time + + try: + sleep_duration = 0.0 + if not action_taken_this_cycle and cycle_duration < 1: + sleep_duration = 1 - cycle_duration + elif cycle_duration < 0.2: + sleep_duration = 0.2 + + if sleep_duration > 0: + await asyncio.sleep(sleep_duration) + + except asyncio.CancelledError: + logger.info(f"{log_prefix} Sleep interrupted, loop likely cancelling.") + raise + + class HeartFChatting: """ 管理一个连续的Plan-Replier-Sender循环 @@ -327,7 +346,7 @@ class HeartFChatting: self._current_cycle.timers = cycle_timers # 防止循环过快消耗资源 - await self._handle_cycle_delay(action_taken, loop_cycle_start_time, self.log_prefix) + await _handle_cycle_delay(action_taken, loop_cycle_start_time, self.log_prefix) # 完成当前循环并保存历史 self._current_cycle.complete_cycle() @@ -715,24 +734,6 @@ class HeartFChatting: if not self._shutting_down: logger.debug(f"{log_prefix} 该次决策耗时: {'; '.join(timer_strings)}") - async def _handle_cycle_delay(self, action_taken_this_cycle: bool, cycle_start_time: float, log_prefix: str): - """处理循环延迟""" - cycle_duration = time.monotonic() - cycle_start_time - - try: - sleep_duration = 0.0 - if not action_taken_this_cycle and cycle_duration < 1: - sleep_duration = 1 - cycle_duration - elif cycle_duration < 0.2: - sleep_duration = 0.2 - - if sleep_duration > 0: - await asyncio.sleep(sleep_duration) - - except asyncio.CancelledError: - logger.info(f"{log_prefix} Sleep interrupted, loop likely cancelling.") - raise - async def _get_submind_thinking(self, cycle_timers: dict) -> str: """ 获取子思维的思考结果 diff --git a/src/plugins/heartFC_chat/heartFC_sender.py b/src/plugins/heartFC_chat/heartFC_sender.py index 9e65edcf..a4103667 100644 --- a/src/plugins/heartFC_chat/heartFC_sender.py +++ b/src/plugins/heartFC_chat/heartFC_sender.py @@ -12,6 +12,22 @@ from src.plugins.chat.utils import calculate_typing_time logger = get_logger("sender") +async def send_message(message: MessageSending) -> None: + """合并后的消息发送函数,包含WS发送和日志记录""" + message_preview = truncate_message(message.processed_plain_text) + + try: + # 直接调用API发送消息 + await 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 # 重新抛出其他异常 + + class HeartFCSender: """管理消息的注册、即时处理、发送和存储,并跟踪思考状态。""" @@ -21,21 +37,6 @@ class HeartFCSender: 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: @@ -73,7 +74,7 @@ class HeartFCSender: 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): + async def type_and_send_message(self, message: MessageSending, typing=False): """ 立即处理、发送并存储单个 MessageSending 消息。 调用此方法前,应先调用 register_thinking 注册对应的思考消息。 @@ -100,7 +101,7 @@ class HeartFCSender: await message.process() - if type: + if typing: typing_time = calculate_typing_time( input_string=message.processed_plain_text, thinking_start_time=message.thinking_start_time, @@ -108,7 +109,7 @@ class HeartFCSender: ) await asyncio.sleep(typing_time) - await self.send_message(message) + await send_message(message) await self.storage.store_message(message, message.chat_stream) except Exception as e: @@ -136,7 +137,7 @@ class HeartFCSender: await asyncio.sleep(0.5) - await self.send_message(message) # 使用现有的发送方法 + await send_message(message) # 使用现有的发送方法 await self.storage.store_message(message, message.chat_stream) # 使用现有的存储方法 except Exception as e: diff --git a/src/plugins/heartFC_chat/heartflow_processor.py b/src/plugins/heartFC_chat/heartflow_processor.py index f7f3819c..5bd63b14 100644 --- a/src/plugins/heartFC_chat/heartflow_processor.py +++ b/src/plugins/heartFC_chat/heartflow_processor.py @@ -12,11 +12,134 @@ from ..chat.chat_stream import chat_manager from ..chat.message_buffer import message_buffer from ..utils.timer_calculator import Timer from src.plugins.person_info.relationship_manager import relationship_manager -from typing import Optional, Tuple +from typing import Optional, Tuple, Dict, Any logger = get_logger("chat") +async def _handle_error(error: Exception, context: str, message: Optional[MessageRecv] = None) -> None: + """统一的错误处理函数 + + Args: + 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(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(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(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" + + +def _check_ban_words(text: str, chat, userinfo) -> bool: + """检查消息是否包含过滤词 + + Args: + text: 待检查的文本 + chat: 聊天对象 + userinfo: 用户信息 + + Returns: + bool: 是否包含过滤词 + """ + for word in global_config.ban_words: + if word in 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(text: str, chat, userinfo) -> bool: + """检查消息是否匹配过滤正则表达式 + + Args: + text: 待检查的文本 + chat: 聊天对象 + userinfo: 用户信息 + + Returns: + bool: 是否匹配过滤正则 + """ + for pattern in global_config.ban_msgs_regex: + if pattern.search(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 + + class HeartFCProcessor: """心流处理器,负责处理接收到的消息并计算兴趣度""" @@ -24,86 +147,7 @@ class HeartFCProcessor: """初始化心流处理器,创建消息存储实例""" self.storage = MessageStorage() - async def _handle_error(self, error: Exception, context: str, message: Optional[MessageRecv] = None) -> None: - """统一的错误处理函数 - - Args: - 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: + async def process_message(self, message_data: Dict[str, Any]) -> None: """处理接收到的原始消息数据 主要流程: @@ -138,7 +182,7 @@ class HeartFCProcessor: await message.process() # 3. 过滤检查 - if self._check_ban_words(message.processed_plain_text, chat, userinfo) or self._check_ban_regex( + if _check_ban_words(message.processed_plain_text, chat, userinfo) or _check_ban_regex( message.raw_message, chat, userinfo ): return @@ -146,7 +190,7 @@ class HeartFCProcessor: # 4. 缓冲检查 buffer_result = await message_buffer.query_buffer_result(message) if not buffer_result: - msg_type = self._get_message_type(message) + msg_type = _get_message_type(message) type_messages = { "text": f"触发缓冲,消息:{message.processed_plain_text}", "image": "触发缓冲,表情包/图片等待中", @@ -160,7 +204,7 @@ class HeartFCProcessor: logger.trace(f"存储成功: {message.processed_plain_text}") # 6. 兴趣度计算与更新 - interested_rate, is_mentioned = await self._calculate_interest(message) + interested_rate, is_mentioned = await _calculate_interest(message) await subheartflow.interest_chatting.increase_interest(value=interested_rate) subheartflow.interest_chatting.add_interest_dict(message, interested_rate, is_mentioned) @@ -175,45 +219,7 @@ class HeartFCProcessor: ) # 8. 关系处理 - await self._process_relationship(message) + await _process_relationship(message) except Exception as e: - 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: - 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): - 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 + await _handle_error(e, "消息处理失败", message) diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 40819f01..9b83f265 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -151,6 +151,96 @@ JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query": Prompt("\n你有以下这些**知识**:\n{prompt_info}\n请你**记住上面的知识**,之后可能会用到。\n", "knowledge_prompt") +async def _build_prompt_focus( + reason, current_mind_info, structured_info, chat_stream, sender_name +) -> tuple[str, str]: + individuality = Individuality.get_instance() + prompt_personality = individuality.get_prompt(x_person=0, level=2) + # 日程构建 + # schedule_prompt = f'''你现在正在做的事情是:{bot_schedule.get_current_num_task(num = 1,time_info = False)}''' + + if chat_stream.group_info: + chat_in_group = True + else: + chat_in_group = False + + message_list_before_now = get_raw_msg_before_timestamp_with_chat( + chat_id=chat_stream.stream_id, + timestamp=time.time(), + limit=global_config.observation_context_size, + ) + + chat_talking_prompt = await build_readable_messages( + message_list_before_now, + replace_bot_name=True, + merge_messages=False, + timestamp_mode="normal", + read_mark=0.0, + truncate=True, + ) + + # 中文高手(新加的好玩功能) + prompt_ger = "" + if random.random() < 0.04: + prompt_ger += "你喜欢用倒装句" + if random.random() < 0.02: + prompt_ger += "你喜欢用反问句" + + reply_styles1 = [ + ("给出日常且口语化的回复,平淡一些", 0.4), # 40%概率 + ("给出非常简短的回复", 0.4), # 40%概率 + ("给出缺失主语的回复,简短", 0.15), # 15%概率 + ("给出带有语病的回复,朴实平淡", 0.05), # 5%概率 + ] + reply_style1_chosen = random.choices( + [style[0] for style in reply_styles1], weights=[style[1] for style in reply_styles1], k=1 + )[0] + + reply_styles2 = [ + ("不要回复的太有条理,可以有个性", 0.6), # 60%概率 + ("不要回复的太有条理,可以复读", 0.15), # 15%概率 + ("回复的认真一些", 0.2), # 20%概率 + ("可以回复单个表情符号", 0.05), # 5%概率 + ] + reply_style2_chosen = random.choices( + [style[0] for style in reply_styles2], weights=[style[1] for style in reply_styles2], k=1 + )[0] + + 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, + bot_name=global_config.BOT_NICKNAME, + prompt_personality=prompt_personality, + chat_target_2=await global_prompt_manager.get_prompt_async("chat_target_group2") + if chat_in_group + else await global_prompt_manager.get_prompt_async("chat_target_private2"), + current_mind_info=current_mind_info, + reply_style2=reply_style2_chosen, + reply_style1=reply_style1_chosen, + reason=reason, + prompt_ger=prompt_ger, + moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), + sender_name=sender_name, + ) + + logger.debug(f"focus_chat_prompt: \n{prompt}") + + return prompt + + class PromptBuilder: def __init__(self): self.prompt_built = "" @@ -170,7 +260,7 @@ class PromptBuilder: return await self._build_prompt_normal(chat_stream, message_txt, sender_name) elif build_mode == "focus": - return await self._build_prompt_focus( + return await _build_prompt_focus( reason, current_mind_info, structured_info, @@ -179,95 +269,6 @@ class PromptBuilder: ) return None - async def _build_prompt_focus( - self, reason, current_mind_info, structured_info, chat_stream, sender_name - ) -> tuple[str, str]: - individuality = Individuality.get_instance() - prompt_personality = individuality.get_prompt(x_person=0, level=2) - # 日程构建 - # schedule_prompt = f'''你现在正在做的事情是:{bot_schedule.get_current_num_task(num = 1,time_info = False)}''' - - if chat_stream.group_info: - chat_in_group = True - else: - chat_in_group = False - - message_list_before_now = get_raw_msg_before_timestamp_with_chat( - chat_id=chat_stream.stream_id, - timestamp=time.time(), - limit=global_config.observation_context_size, - ) - - chat_talking_prompt = await build_readable_messages( - message_list_before_now, - replace_bot_name=True, - merge_messages=False, - timestamp_mode="normal", - read_mark=0.0, - truncate=True, - ) - - # 中文高手(新加的好玩功能) - prompt_ger = "" - if random.random() < 0.04: - prompt_ger += "你喜欢用倒装句" - if random.random() < 0.02: - prompt_ger += "你喜欢用反问句" - - reply_styles1 = [ - ("给出日常且口语化的回复,平淡一些", 0.4), # 40%概率 - ("给出非常简短的回复", 0.4), # 40%概率 - ("给出缺失主语的回复,简短", 0.15), # 15%概率 - ("给出带有语病的回复,朴实平淡", 0.05), # 5%概率 - ] - reply_style1_chosen = random.choices( - [style[0] for style in reply_styles1], weights=[style[1] for style in reply_styles1], k=1 - )[0] - - reply_styles2 = [ - ("不要回复的太有条理,可以有个性", 0.6), # 60%概率 - ("不要回复的太有条理,可以复读", 0.15), # 15%概率 - ("回复的认真一些", 0.2), # 20%概率 - ("可以回复单个表情符号", 0.05), # 5%概率 - ] - reply_style2_chosen = random.choices( - [style[0] for style in reply_styles2], weights=[style[1] for style in reply_styles2], k=1 - )[0] - - 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, - bot_name=global_config.BOT_NICKNAME, - prompt_personality=prompt_personality, - chat_target_2=await global_prompt_manager.get_prompt_async("chat_target_group2") - if chat_in_group - else await global_prompt_manager.get_prompt_async("chat_target_private2"), - current_mind_info=current_mind_info, - reply_style2=reply_style2_chosen, - reply_style1=reply_style1_chosen, - reason=reason, - prompt_ger=prompt_ger, - moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), - sender_name=sender_name, - ) - - logger.debug(f"focus_chat_prompt: \n{prompt}") - - return prompt - async def _build_prompt_normal(self, chat_stream, message_txt: str, sender_name: str = "某人") -> tuple[str, str]: individuality = Individuality.get_instance() prompt_personality = individuality.get_prompt(x_person=2, level=2) diff --git a/src/plugins/knowledge/src/qa_manager.py b/src/plugins/knowledge/src/qa_manager.py index a09879a1..11067d0e 100644 --- a/src/plugins/knowledge/src/qa_manager.py +++ b/src/plugins/knowledge/src/qa_manager.py @@ -27,7 +27,7 @@ class QAManager: self.kg_manager = kg_manager self.llm_client_list = { "embedding": llm_client_embedding, - "filter": llm_client_filter, + "message_filter": llm_client_filter, "qa": llm_client_qa, } diff --git a/src/plugins/respon_info_catcher/info_catcher.py b/src/plugins/respon_info_catcher/info_catcher.py index 5cb67a16..08be4a76 100644 --- a/src/plugins/respon_info_catcher/info_catcher.py +++ b/src/plugins/respon_info_catcher/info_catcher.py @@ -185,32 +185,24 @@ class InfoCatcher: try: # 将消息对象转换为可序列化的字典喵~ - thinking_log_data = { - "chat_id": self.chat_id, - # "response_mode": self.response_mode, # 这个也删掉喵~ - "trigger_text": self.trigger_response_text, - "response_text": self.response_text, - "trigger_info": { + thinking_log_data = {"chat_id": self.chat_id, "trigger_text": self.trigger_response_text, + "response_text": self.response_text, "trigger_info": { "time": self.trigger_response_time, "message": self.message_to_dict(self.trigger_response_message), - }, - "response_info": { + }, "response_info": { "time": self.response_time, "message": self.response_messages, - }, - "timing_results": self.timing_results, - "chat_history": self.message_list_to_dict(self.chat_history), - "chat_history_in_thinking": self.message_list_to_dict(self.chat_history_in_thinking), - "chat_history_after_response": self.message_list_to_dict(self.chat_history_after_response), - } + }, "timing_results": self.timing_results, "chat_history": self.message_list_to_dict(self.chat_history), + "chat_history_in_thinking": self.message_list_to_dict(self.chat_history_in_thinking), + "chat_history_after_response": self.message_list_to_dict( + self.chat_history_after_response), "heartflow_data": self.heartflow_data, + "reasoning_data": self.reasoning_data} # 根据不同的响应模式添加相应的数据喵~ # 现在直接都加上去好了喵~ # if self.response_mode == "heart_flow": # thinking_log_data["mode_specific_data"] = self.heartflow_data # elif self.response_mode == "reasoning": # thinking_log_data["mode_specific_data"] = self.reasoning_data - thinking_log_data["heartflow_data"] = self.heartflow_data - thinking_log_data["reasoning_data"] = self.reasoning_data # 将数据插入到 thinking_log 集合中喵~ db.thinking_log.insert_one(thinking_log_data) diff --git a/src/plugins/schedule/schedule_generator.py b/src/plugins/schedule/schedule_generator.py index 761fcb7d..ee7bdee1 100644 --- a/src/plugins/schedule/schedule_generator.py +++ b/src/plugins/schedule/schedule_generator.py @@ -30,6 +30,7 @@ class ScheduleGenerator: def __init__(self): # 使用离线LLM模型 + self.enable_output = None self.llm_scheduler_all = LLMRequest( model=global_config.llm_reasoning, temperature=global_config.SCHEDULE_TEMPERATURE + 0.3, diff --git a/src/plugins/utils/chat_message_builder.py b/src/plugins/utils/chat_message_builder.py index a7eef443..75fca69c 100644 --- a/src/plugins/utils/chat_message_builder.py +++ b/src/plugins/utils/chat_message_builder.py @@ -123,7 +123,7 @@ def num_new_messages_since(chat_id: str, timestamp_start: float = 0.0, timestamp return 0 # 起始时间大于等于结束时间,没有新消息 filter_query = {"chat_id": chat_id, "time": {"$gt": timestamp_start, "$lt": _timestamp_end}} - return count_messages(filter=filter_query) + return count_messages(message_filter=filter_query) def num_new_messages_since_with_users( @@ -137,7 +137,7 @@ def num_new_messages_since_with_users( "time": {"$gt": timestamp_start, "$lt": timestamp_end}, "user_id": {"$in": person_ids}, } - return count_messages(filter=filter_query) + return count_messages(message_filter=filter_query) async def _build_readable_messages_internal( @@ -227,7 +227,7 @@ async def _build_readable_messages_internal( replace_content = "......(太长了)" truncated_content = content - if limit > 0 and original_len > limit: + if 0 < limit < original_len: truncated_content = f"{content[:limit]}{replace_content}" message_details.append((timestamp, name, truncated_content)) diff --git a/src/plugins/willing/mode_custom.py b/src/plugins/willing/mode_custom.py index c3a5c307..4b2e8f3c 100644 --- a/src/plugins/willing/mode_custom.py +++ b/src/plugins/willing/mode_custom.py @@ -2,5 +2,23 @@ from .willing_manager import BaseWillingManager class CustomWillingManager(BaseWillingManager): + async def async_task_starter(self) -> None: + pass + + async def before_generate_reply_handle(self, message_id: str): + pass + + async def after_generate_reply_handle(self, message_id: str): + pass + + async def not_reply_handle(self, message_id: str): + pass + + async def get_reply_probability(self, message_id: str): + pass + + async def bombing_buffer_message_handle(self, message_id: str): + pass + def __init__(self): super().__init__() From 3d001da30e9f69295b8031aeab9ba056772a3719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E7=8C=AB?= Date: Thu, 1 May 2025 06:07:59 +0900 Subject: [PATCH 08/67] QA: Refactor similarity calculation and improve state management logic --- src/heart_flow/mai_state_manager.py | 5 +- src/heart_flow/sub_mind.py | 4 +- src/heart_flow/subheartflow_manager.py | 70 ++++++++++---------- src/plugins/PFC/pfc.py | 41 ++++++------ src/plugins/chat/bot.py | 2 +- src/plugins/knowledge/src/prompt_template.py | 8 +-- src/plugins/models/utils_model.py | 47 ++++++------- 7 files changed, 91 insertions(+), 86 deletions(-) diff --git a/src/heart_flow/mai_state_manager.py b/src/heart_flow/mai_state_manager.py index 29277820..d289a94a 100644 --- a/src/heart_flow/mai_state_manager.py +++ b/src/heart_flow/mai_state_manager.py @@ -62,6 +62,7 @@ class MaiState(enum.Enum): return MAX_NORMAL_CHAT_NUM_NORMAL elif self == MaiState.FOCUSED_CHAT: return MAX_NORMAL_CHAT_NUM_FOCUSED + return None def get_focused_chat_max_num(self): # 调试用 @@ -76,6 +77,7 @@ class MaiState(enum.Enum): return MAX_FOCUSED_CHAT_NUM_NORMAL elif self == MaiState.FOCUSED_CHAT: return MAX_FOCUSED_CHAT_NUM_FOCUSED + return None class MaiStateInfo: @@ -135,7 +137,8 @@ class MaiStateManager: def __init__(self): pass - def check_and_decide_next_state(self, current_state_info: MaiStateInfo) -> Optional[MaiState]: + @staticmethod + def check_and_decide_next_state(current_state_info: MaiStateInfo) -> Optional[MaiState]: """ 根据当前状态和规则检查是否需要转换状态,并决定下一个状态。 diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index f1716d24..09d7b936 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -78,7 +78,7 @@ def calculate_replacement_probability(similarity: float) -> float: # p = 3.5 * s - 1.4 probability = 3.5 * similarity - 1.4 return max(0.0, probability) - elif 0.6 < similarity < 0.9: + else: # 0.6 < similarity < 0.9 # p = s + 0.1 probability = similarity + 0.1 return min(1.0, max(0.0, probability)) @@ -169,7 +169,7 @@ class SubMind: last_cycle = history_cycle[-1] if history_cycle else None # 上一次决策信息 - if last_cycle != None: + if last_cycle is not None: last_action = last_cycle.action_type last_reasoning = last_cycle.reasoning is_replan = last_cycle.replanned diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index 16f36dcc..30119cca 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -32,6 +32,40 @@ INACTIVE_THRESHOLD_SECONDS = 3600 # 子心流不活跃超时时间(秒) NORMAL_CHAT_TIMEOUT_SECONDS = 30 * 60 # 30分钟 +async def _try_set_subflow_absent_internal(subflow: "SubHeartflow", log_prefix: str) -> bool: + """ + 尝试将给定的子心流对象状态设置为 ABSENT (内部方法,不处理锁)。 + + Args: + subflow: 子心流对象。 + log_prefix: 用于日志记录的前缀 (例如 "[子心流管理]" 或 "[停用]")。 + + Returns: + bool: 如果状态成功变为 ABSENT 或原本就是 ABSENT,返回 True;否则返回 False。 + """ + flow_id = subflow.subheartflow_id + stream_name = chat_manager.get_stream_name(flow_id) or flow_id + + if subflow.chat_state.chat_status != ChatState.ABSENT: + logger.debug(f"{log_prefix} 设置 {stream_name} 状态为 ABSENT") + try: + await subflow.change_chat_state(ChatState.ABSENT) + # 再次检查以确认状态已更改 (change_chat_state 内部应确保) + if subflow.chat_state.chat_status == ChatState.ABSENT: + return True + else: + logger.warning( + f"{log_prefix} 调用 change_chat_state 后,{stream_name} 状态仍为 {subflow.chat_state.chat_status.value}" + ) + return False + except Exception as e: + logger.error(f"{log_prefix} 设置 {stream_name} 状态为 ABSENT 时失败: {e}", exc_info=True) + return False + else: + logger.debug(f"{log_prefix} {stream_name} 已是 ABSENT 状态") + return True # 已经是目标状态,视为成功 + + class SubHeartflowManager: """管理所有活跃的 SubHeartflow 实例。""" @@ -109,38 +143,6 @@ class SubHeartflowManager: return None # --- 新增:内部方法,用于尝试将单个子心流设置为 ABSENT --- - async def _try_set_subflow_absent_internal(self, subflow: "SubHeartflow", log_prefix: str) -> bool: - """ - 尝试将给定的子心流对象状态设置为 ABSENT (内部方法,不处理锁)。 - - Args: - subflow: 子心流对象。 - log_prefix: 用于日志记录的前缀 (例如 "[子心流管理]" 或 "[停用]")。 - - Returns: - bool: 如果状态成功变为 ABSENT 或原本就是 ABSENT,返回 True;否则返回 False。 - """ - flow_id = subflow.subheartflow_id - stream_name = chat_manager.get_stream_name(flow_id) or flow_id - - if subflow.chat_state.chat_status != ChatState.ABSENT: - logger.debug(f"{log_prefix} 设置 {stream_name} 状态为 ABSENT") - try: - await subflow.change_chat_state(ChatState.ABSENT) - # 再次检查以确认状态已更改 (change_chat_state 内部应确保) - if subflow.chat_state.chat_status == ChatState.ABSENT: - return True - else: - logger.warning( - f"{log_prefix} 调用 change_chat_state 后,{stream_name} 状态仍为 {subflow.chat_state.chat_status.value}" - ) - return False - except Exception as e: - logger.error(f"{log_prefix} 设置 {stream_name} 状态为 ABSENT 时失败: {e}", exc_info=True) - return False - else: - logger.debug(f"{log_prefix} {stream_name} 已是 ABSENT 状态") - return True # 已经是目标状态,视为成功 # --- 结束新增 --- @@ -154,7 +156,7 @@ class SubHeartflowManager: logger.info(f"{log_prefix} 正在停止 {stream_name}, 原因: {reason}") # 调用内部方法处理状态变更 - success = await self._try_set_subflow_absent_internal(subheartflow, log_prefix) + success = await _try_set_subflow_absent_internal(subheartflow, log_prefix) return success # 锁在此处自动释放 @@ -241,7 +243,7 @@ class SubHeartflowManager: # 记录原始状态,以便统计实际改变的数量 original_state_was_absent = subflow.chat_state.chat_status == ChatState.ABSENT - success = await self._try_set_subflow_absent_internal(subflow, log_prefix) + success = await _try_set_subflow_absent_internal(subflow, log_prefix) # 如果成功设置为 ABSENT 且原始状态不是 ABSENT,则计数 if success and not original_state_was_absent: diff --git a/src/plugins/PFC/pfc.py b/src/plugins/PFC/pfc.py index e12cb242..6cb1fe83 100644 --- a/src/plugins/PFC/pfc.py +++ b/src/plugins/PFC/pfc.py @@ -15,6 +15,26 @@ if TYPE_CHECKING: logger = get_module_logger("pfc") +def _calculate_similarity(goal1: str, goal2: str) -> float: + """简单计算两个目标之间的相似度 + + 这里使用一个简单的实现,实际可以使用更复杂的文本相似度算法 + + Args: + goal1: 第一个目标 + goal2: 第二个目标 + + Returns: + float: 相似度得分 (0-1) + """ + # 简单实现:检查重叠字数比例 + words1 = set(goal1) + words2 = set(goal2) + overlap = len(words1.intersection(words2)) + total = len(words1.union(words2)) + return overlap / total if total > 0 else 0 + + class GoalAnalyzer: """对话目标分析器""" @@ -166,7 +186,7 @@ class GoalAnalyzer: """ # 检查新目标是否与现有目标相似 for i, (existing_goal, _, _) in enumerate(self.goals): - if self._calculate_similarity(new_goal, existing_goal) > 0.7: # 相似度阈值 + if _calculate_similarity(new_goal, existing_goal) > 0.7: # 相似度阈值 # 更新现有目标 self.goals[i] = (new_goal, method, reasoning) # 将此目标移到列表前面(最主要的位置) @@ -180,25 +200,6 @@ class GoalAnalyzer: if len(self.goals) > self.max_goals: self.goals.pop() # 移除最老的目标 - def _calculate_similarity(self, goal1: str, goal2: str) -> float: - """简单计算两个目标之间的相似度 - - 这里使用一个简单的实现,实际可以使用更复杂的文本相似度算法 - - Args: - goal1: 第一个目标 - goal2: 第二个目标 - - Returns: - float: 相似度得分 (0-1) - """ - # 简单实现:检查重叠字数比例 - words1 = set(goal1) - words2 = set(goal2) - overlap = len(words1.intersection(words2)) - total = len(words1.union(words2)) - return overlap / total if total > 0 else 0 - async def get_all_goals(self) -> List[Tuple[str, str, str]]: """获取所有当前目标 diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 7ca1483c..89e171ba 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -84,7 +84,7 @@ class ChatBot: return # 群聊黑名单拦截 - if groupinfo != None and groupinfo.group_id not in global_config.talk_allowed_groups: + if groupinfo is not None and groupinfo.group_id not in global_config.talk_allowed_groups: logger.trace(f"群{groupinfo.group_id}被禁止回复") return diff --git a/src/plugins/knowledge/src/prompt_template.py b/src/plugins/knowledge/src/prompt_template.py index 18a5002e..14a36008 100644 --- a/src/plugins/knowledge/src/prompt_template.py +++ b/src/plugins/knowledge/src/prompt_template.py @@ -1,5 +1,3 @@ -from typing import List - from .llm_client import LLMMessage entity_extract_system_prompt = """你是一个性能优异的实体提取系统。请从段落中提取出所有实体,并以JSON列表的形式输出。 @@ -13,7 +11,7 @@ entity_extract_system_prompt = """你是一个性能优异的实体提取系统 """ -def build_entity_extract_context(paragraph: str) -> List[LLMMessage]: +def build_entity_extract_context(paragraph: str) -> list[LLMMessage]: messages = [ LLMMessage("system", entity_extract_system_prompt).to_dict(), LLMMessage("user", f"""段落:\n```\n{paragraph}```""").to_dict(), @@ -38,7 +36,7 @@ rdf_triple_extract_system_prompt = """你是一个性能优异的RDF(资源描 """ -def build_rdf_triple_extract_context(paragraph: str, entities: str) -> List[LLMMessage]: +def build_rdf_triple_extract_context(paragraph: str, entities: str) -> list[LLMMessage]: messages = [ LLMMessage("system", rdf_triple_extract_system_prompt).to_dict(), LLMMessage("user", f"""段落:\n```\n{paragraph}```\n\n实体列表:\n```\n{entities}```""").to_dict(), @@ -56,7 +54,7 @@ qa_system_prompt = """ """ -def build_qa_context(question: str, knowledge: list[(str, str, str)]) -> List[LLMMessage]: +def build_qa_context(question: str, knowledge: list[tuple[str, str, str]]) -> list[LLMMessage]: knowledge = "\n".join([f"{i + 1}. 相关性:{k[0]}\n{k[1]}" for i, k in enumerate(knowledge)]) messages = [ LLMMessage("system", qa_system_prompt).to_dict(), diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py index e421641c..6b9c0416 100644 --- a/src/plugins/models/utils_model.py +++ b/src/plugins/models/utils_model.py @@ -65,6 +65,28 @@ error_code_mapping = { } +async def _safely_record(request_content: Dict[str, Any], payload: Dict[str, Any]): + image_base64: str = request_content.get("image_base64") + image_format: str = request_content.get("image_format") + if ( + image_base64 + and payload + and isinstance(payload, dict) + and "messages" in payload + and len(payload["messages"]) > 0 + ): + if isinstance(payload["messages"][0], dict) and "content" in payload["messages"][0]: + content = payload["messages"][0]["content"] + if isinstance(content, list) and len(content) > 1 and "image_url" in content[1]: + payload["messages"][0]["content"][1]["image_url"]["url"] = ( + f"data:image/{image_format.lower() if image_format else 'jpeg'};base64," + f"{image_base64[:10]}...{image_base64[-10:]}" + ) + # if isinstance(content, str) and len(content) > 100: + # payload["messages"][0]["content"] = content[:100] + return payload + + class LLMRequest: # 定义需要转换的模型列表,作为类变量避免重复 MODELS_NEEDING_TRANSFORMATION = [ @@ -551,7 +573,7 @@ class LLMRequest: f"模型 {self.model_name} HTTP响应错误达到最大重试次数: 状态码: {exception.status}, 错误: {exception.message}" ) # 安全地检查和记录请求详情 - handled_payload = await self._safely_record(request_content, payload) + handled_payload = await _safely_record(request_content, payload) logger.critical(f"请求头: {await self._build_headers(no_key=True)} 请求体: {handled_payload}") raise RuntimeError( f"模型 {self.model_name} API请求失败: 状态码 {exception.status}, {exception.message}" @@ -565,31 +587,10 @@ class LLMRequest: else: logger.critical(f"模型 {self.model_name} 请求失败: {str(exception)}") # 安全地检查和记录请求详情 - handled_payload = await self._safely_record(request_content, payload) + handled_payload = await _safely_record(request_content, payload) logger.critical(f"请求头: {await self._build_headers(no_key=True)} 请求体: {handled_payload}") raise RuntimeError(f"模型 {self.model_name} API请求失败: {str(exception)}") - async def _safely_record(self, request_content: Dict[str, Any], payload: Dict[str, Any]): - image_base64: str = request_content.get("image_base64") - image_format: str = request_content.get("image_format") - if ( - image_base64 - and payload - and isinstance(payload, dict) - and "messages" in payload - and len(payload["messages"]) > 0 - ): - if isinstance(payload["messages"][0], dict) and "content" in payload["messages"][0]: - content = payload["messages"][0]["content"] - if isinstance(content, list) and len(content) > 1 and "image_url" in content[1]: - payload["messages"][0]["content"][1]["image_url"]["url"] = ( - f"data:image/{image_format.lower() if image_format else 'jpeg'};base64," - f"{image_base64[:10]}...{image_base64[-10:]}" - ) - # if isinstance(content, str) and len(content) > 100: - # payload["messages"][0]["content"] = content[:100] - return payload - async def _transform_parameters(self, params: dict) -> dict: """ 根据模型名称转换参数: From 263e8d196ad5ecda2312dba284f301da4616e134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E7=8C=AB?= Date: Thu, 1 May 2025 06:55:05 +0900 Subject: [PATCH 09/67] fix: Update type hints to use newer Python syntax MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace Dict, List, Optional with dict, list, < /dev/null | None syntax - Fix abstract method implementation in message.py - Improve type annotations and function return types - Remove unreachable code in get_current_task_tool.py - Refactor HTML elements to use style attributes 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 16 ++-- scripts/import_openie.py | 7 +- src/api/config_api.py | 28 +++---- src/common/logger.py | 6 +- src/common/message_repository.py | 8 +- src/config/config.py | 4 +- src/do_tool/not_used/change_mood.py | 6 +- src/do_tool/not_used/change_relationship.py | 2 +- src/do_tool/not_used/get_current_task.py | 9 ++- src/do_tool/not_used/mid_chat_mem.py | 6 +- src/do_tool/not_used/send_emoji.py | 2 +- src/do_tool/tool_can_use/README.md | 2 +- src/do_tool/tool_can_use/base_tool.py | 12 +-- .../tool_can_use/compare_numbers_tool.py | 7 +- src/do_tool/tool_can_use/get_knowledge.py | 7 +- src/do_tool/tool_can_use/get_memory.py | 1 - src/do_tool/tool_can_use/get_time_date.py | 1 - .../tool_can_use/lpmm_get_knowledge.py | 1 - src/do_tool/tool_use.py | 1 - src/individuality/scene.py | 12 +-- src/plugins/PFC/chat_observer.py | 1 + src/plugins/PFC/pfc_manager.py | 1 + src/plugins/PFC/pfc_utils.py | 1 + src/plugins/PFC/reply_checker.py | 2 + src/plugins/chat/message.py | 80 ++++++++----------- src/plugins/chat/message_sender.py | 16 ++-- src/plugins/chat/utils.py | 22 +++-- src/plugins/memory_system/Hippocampus.py | 4 +- src/plugins/schedule/schedule_generator.py | 3 +- 29 files changed, 125 insertions(+), 143 deletions(-) diff --git a/README.md b/README.md index 58cb82c7..f349e0ca 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 麦麦!MaiCore-MaiMBot (编辑中)
-
+
![Python Version](https://img.shields.io/badge/Python-3.10+-blue) ![License](https://img.shields.io/github/license/SengokuCola/MaiMBot?label=协议) @@ -12,7 +12,7 @@
-

+

Logo @@ -21,8 +21,8 @@ 画师:略nd -

MaiBot(麦麦)

-

+

MaiBot(麦麦)

+

一款专注于 群组聊天 的赛博网友
探索本项目的文档 » @@ -50,7 +50,7 @@ - 🧠 **持久记忆系统**:基于MongoDB的长期记忆存储 - 🔄 **动态人格系统**:自适应的性格特征 -

+
麦麦演示视频
@@ -97,9 +97,9 @@ - [四群](https://qm.qq.com/q/wlH5eT8OmQ) 729957033【已满】 -
-

📚 文档

-
+ +## 📚 文档 + ### (部分内容可能过时,请注意版本对应) diff --git a/scripts/import_openie.py b/scripts/import_openie.py index 26cbd8ce..595f22ec 100644 --- a/scripts/import_openie.py +++ b/scripts/import_openie.py @@ -8,7 +8,6 @@ import sys import os sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) -from typing import Dict, List from src.plugins.knowledge.src.lpmmconfig import PG_NAMESPACE, global_config from src.plugins.knowledge.src.embedding_store import EmbeddingManager @@ -26,8 +25,8 @@ logger = get_module_logger("LPMM知识库-OpenIE导入") def hash_deduplicate( - raw_paragraphs: Dict[str, str], - triple_list_data: Dict[str, List[List[str]]], + raw_paragraphs: dict[str, str], + triple_list_data: dict[str, list[list[str]]], stored_pg_hashes: set, stored_paragraph_hashes: set, ): @@ -126,7 +125,7 @@ def main(): ) # 初始化Embedding库 - embed_manager = embed_manager = EmbeddingManager(llm_client_list[global_config["embedding"]["provider"]]) + embed_manager = EmbeddingManager(llm_client_list[global_config["embedding"]["provider"]]) logger.info("正在从文件加载Embedding库") try: embed_manager.load_from_file() diff --git a/src/api/config_api.py b/src/api/config_api.py index 025888d8..6ecd4e6d 100644 --- a/src/api/config_api.py +++ b/src/api/config_api.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional +from typing import List, Optional import strawberry # from packaging.version import Version, InvalidVersion @@ -128,22 +128,22 @@ class BotConfig: enable_pfc_chatting: bool # 是否启用PFC聊天 # 模型配置 - llm_reasoning: Dict[str, str] # LLM推理 - # llm_reasoning_minor: Dict[str, str] - llm_normal: Dict[str, str] # LLM普通 - llm_topic_judge: Dict[str, str] # LLM话题判断 - llm_summary: Dict[str, str] # LLM话题总结 - llm_emotion_judge: Dict[str, str] # LLM情感判断 - embedding: Dict[str, str] # 嵌入 - vlm: Dict[str, str] # VLM - moderation: Dict[str, str] # 审核 + llm_reasoning: dict[str, str] # LLM推理 + # llm_reasoning_minor: dict[str, str] + llm_normal: dict[str, str] # LLM普通 + llm_topic_judge: dict[str, str] # LLM话题判断 + llm_summary: dict[str, str] # LLM话题总结 + llm_emotion_judge: dict[str, str] # LLM情感判断 + embedding: dict[str, str] # 嵌入 + vlm: dict[str, str] # VLM + moderation: dict[str, str] # 审核 # 实验性 - llm_observation: Dict[str, str] # LLM观察 - llm_sub_heartflow: Dict[str, str] # LLM子心流 - llm_heartflow: Dict[str, str] # LLM心流 + llm_observation: dict[str, str] # LLM观察 + llm_sub_heartflow: dict[str, str] # LLM子心流 + llm_heartflow: dict[str, str] # LLM心流 - api_urls: Dict[str, str] # API URLs + api_urls: dict[str, str] # API URLs @strawberry.type diff --git a/src/common/logger.py b/src/common/logger.py index a0d621d9..a82c6d88 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -1,5 +1,5 @@ from loguru import logger -from typing import Dict, Optional, Union, List, Tuple +from typing import Optional, Union, List, Tuple import sys import os from types import ModuleType @@ -75,8 +75,8 @@ if default_handler_id is not None: LoguruLogger = logger.__class__ # 全局注册表:记录模块与处理器ID的映射 -_handler_registry: Dict[str, List[int]] = {} -_custom_style_handlers: Dict[Tuple[str, str], List[int]] = {} # 记录自定义样式处理器ID +_handler_registry: dict[str, List[int]] = {} +_custom_style_handlers: dict[Tuple[str, str], List[int]] = {} # 记录自定义样式处理器ID # 获取日志存储根地址 current_file_path = Path(__file__).resolve() diff --git a/src/common/message_repository.py b/src/common/message_repository.py index 72643f91..11bd6095 100644 --- a/src/common/message_repository.py +++ b/src/common/message_repository.py @@ -7,11 +7,11 @@ logger = get_module_logger(__name__) def find_messages( - message_filter: Dict[str, Any], + message_filter: dict[str, Any], sort: Optional[List[tuple[str, int]]] = None, limit: int = 0, limit_mode: str = "latest", -) -> List[Dict[str, Any]]: +) -> List[dict[str, Any]]: """ 根据提供的过滤器、排序和限制条件查找消息。 @@ -26,7 +26,7 @@ def find_messages( """ try: query = db.messages.find(message_filter) - results: List[Dict[str, Any]] = [] + results: List[dict[str, Any]] = [] if limit > 0: if limit_mode == "earliest": @@ -56,7 +56,7 @@ def find_messages( return [] -def count_messages(message_filter: Dict[str, Any]) -> int: +def count_messages(message_filter: dict[str, Any]) -> int: """ 根据提供的过滤器计算消息数量。 diff --git a/src/config/config.py b/src/config/config.py index fbf558a3..033d57f5 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -271,8 +271,8 @@ class BotConfig: enable_pfc_chatting: bool = False # 是否启用PFC聊天 # 模型配置 - llm_reasoning: Dict[str, str] = field(default_factory=lambda: {}) - # llm_reasoning_minor: Dict[str, str] = field(default_factory=lambda: {}) + llm_reasoning: dict[str, str] = field(default_factory=lambda: {}) + # llm_reasoning_minor: dict[str, str] = field(default_factory=lambda: {}) llm_normal: Dict[str, str] = field(default_factory=lambda: {}) llm_topic_judge: Dict[str, str] = field(default_factory=lambda: {}) llm_summary: Dict[str, str] = field(default_factory=lambda: {}) diff --git a/src/do_tool/not_used/change_mood.py b/src/do_tool/not_used/change_mood.py index 430561a2..5dee6ac9 100644 --- a/src/do_tool/not_used/change_mood.py +++ b/src/do_tool/not_used/change_mood.py @@ -3,7 +3,7 @@ from src.config.config import global_config from src.common.logger_manager import get_logger from src.plugins.moods.moods import MoodManager -from typing import Dict, Any +from typing import Any logger = get_logger("change_mood_tool") @@ -22,7 +22,7 @@ class ChangeMoodTool(BaseTool): "required": ["text", "response_set"], } - async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]: + async def execute(self, function_args: dict[str, Any], message_txt: str = "") -> dict[str, Any]: """执行心情改变 Args: @@ -30,7 +30,7 @@ class ChangeMoodTool(BaseTool): message_txt: 原始消息文本 Returns: - Dict: 工具执行结果 + dict: 工具执行结果 """ try: response_set = function_args.get("response_set") diff --git a/src/do_tool/not_used/change_relationship.py b/src/do_tool/not_used/change_relationship.py index 4af32fb8..ab91489f 100644 --- a/src/do_tool/not_used/change_relationship.py +++ b/src/do_tool/not_used/change_relationship.py @@ -19,7 +19,7 @@ class RelationshipTool(BaseTool): "required": ["text", "changed_value", "reason"], } - async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> dict: + async def execute(self, function_args: dict[str, Any], message_txt: str = "") -> dict: """执行工具功能 Args: diff --git a/src/do_tool/not_used/get_current_task.py b/src/do_tool/not_used/get_current_task.py index d5660f6a..30184d67 100644 --- a/src/do_tool/not_used/get_current_task.py +++ b/src/do_tool/not_used/get_current_task.py @@ -1,7 +1,7 @@ from src.do_tool.tool_can_use.base_tool import BaseTool from src.plugins.schedule.schedule_generator import bot_schedule from src.common.logger import get_module_logger -from typing import Dict, Any +from typing import Any from datetime import datetime logger = get_module_logger("get_current_task_tool") @@ -21,7 +21,7 @@ class GetCurrentTaskTool(BaseTool): "required": ["start_time", "end_time"], } - async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]: + async def execute(self, function_args: dict[str, Any], message_txt: str = "") -> dict[str, Any]: """执行获取当前任务或指定时间段的日程信息 Args: @@ -29,7 +29,7 @@ class GetCurrentTaskTool(BaseTool): message_txt: 原始消息文本,此工具不使用 Returns: - Dict: 工具执行结果 + dict: 工具执行结果 """ start_time = function_args.get("start_time") end_time = function_args.get("end_time") @@ -55,5 +55,6 @@ class GetCurrentTaskTool(BaseTool): task_info = "\n".join(task_list) else: task_info = f"在 {start_time} 到 {end_time} 之间没有找到日程信息" - + else: + task_info = "请提供有效的开始时间和结束时间" return {"name": "get_current_task", "content": f"日程信息: {task_info}"} diff --git a/src/do_tool/not_used/mid_chat_mem.py b/src/do_tool/not_used/mid_chat_mem.py index 71726a57..0340df13 100644 --- a/src/do_tool/not_used/mid_chat_mem.py +++ b/src/do_tool/not_used/mid_chat_mem.py @@ -1,6 +1,6 @@ from src.do_tool.tool_can_use.base_tool import BaseTool from src.common.logger import get_module_logger -from typing import Dict, Any +from typing import Any logger = get_module_logger("get_mid_memory_tool") @@ -18,7 +18,7 @@ class GetMidMemoryTool(BaseTool): "required": ["id"], } - async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]: + async def execute(self, function_args: dict[str, Any], message_txt: str = "") -> dict[str, Any]: """执行记忆获取 Args: @@ -26,7 +26,7 @@ class GetMidMemoryTool(BaseTool): message_txt: 原始消息文本 Returns: - Dict: 工具执行结果 + dict: 工具执行结果 """ try: id = function_args.get("id") diff --git a/src/do_tool/not_used/send_emoji.py b/src/do_tool/not_used/send_emoji.py index 3c6c8a3f..9bf2dd48 100644 --- a/src/do_tool/not_used/send_emoji.py +++ b/src/do_tool/not_used/send_emoji.py @@ -17,7 +17,7 @@ class SendEmojiTool(BaseTool): "required": ["text"], } - async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]: + async def execute(self, function_args: dict[str, Any], message_txt: str = "") -> dict[str, Any]: text = function_args.get("text", message_txt) return { "name": "send_emoji", diff --git a/src/do_tool/tool_can_use/README.md b/src/do_tool/tool_can_use/README.md index 15c77188..0b746b4e 100644 --- a/src/do_tool/tool_can_use/README.md +++ b/src/do_tool/tool_can_use/README.md @@ -42,7 +42,7 @@ class MyNewTool(BaseTool): message_txt: 原始消息文本 Returns: - Dict: 包含执行结果的字典,必须包含name和content字段 + dict: 包含执行结果的字典,必须包含name和content字段 """ # 实现工具逻辑 result = f"工具执行结果: {function_args.get('param1')}" diff --git a/src/do_tool/tool_can_use/base_tool.py b/src/do_tool/tool_can_use/base_tool.py index 1dd15baf..b01e543e 100644 --- a/src/do_tool/tool_can_use/base_tool.py +++ b/src/do_tool/tool_can_use/base_tool.py @@ -22,11 +22,11 @@ class BaseTool: parameters = None @classmethod - def get_tool_definition(cls) -> Dict[str, Any]: + def get_tool_definition(cls) -> dict[str, Any]: """获取工具定义,用于LLM工具调用 Returns: - Dict: 工具定义字典 + dict: 工具定义字典 """ if not cls.name or not cls.description or not cls.parameters: raise NotImplementedError(f"工具类 {cls.__name__} 必须定义 name, description 和 parameters 属性") @@ -36,14 +36,14 @@ class BaseTool: "function": {"name": cls.name, "description": cls.description, "parameters": cls.parameters}, } - async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]: + async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]: """执行工具函数 Args: function_args: 工具调用参数 Returns: - Dict: 工具执行结果 + dict: 工具执行结果 """ raise NotImplementedError("子类必须实现execute方法") @@ -88,11 +88,11 @@ def discover_tools(): logger.info(f"工具发现完成,共注册 {len(TOOL_REGISTRY)} 个工具") -def get_all_tool_definitions() -> List[Dict[str, Any]]: +def get_all_tool_definitions() -> List[dict[str, Any]]: """获取所有已注册工具的定义 Returns: - List[Dict]: 工具定义列表 + List[dict]: 工具定义列表 """ return [tool_class().get_tool_definition() for tool_class in TOOL_REGISTRY.values()] diff --git a/src/do_tool/tool_can_use/compare_numbers_tool.py b/src/do_tool/tool_can_use/compare_numbers_tool.py index 1fbd812a..ef037de9 100644 --- a/src/do_tool/tool_can_use/compare_numbers_tool.py +++ b/src/do_tool/tool_can_use/compare_numbers_tool.py @@ -1,6 +1,6 @@ from src.do_tool.tool_can_use.base_tool import BaseTool from src.common.logger import get_module_logger -from typing import Dict, Any +from typing import Any logger = get_module_logger("compare_numbers_tool") @@ -19,15 +19,14 @@ class CompareNumbersTool(BaseTool): "required": ["num1", "num2"], } - async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]: + async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]: """执行比较两个数的大小 Args: function_args: 工具参数 - message_txt: 原始消息文本 Returns: - Dict: 工具执行结果 + dict: 工具执行结果 """ try: num1 = function_args.get("num1") diff --git a/src/do_tool/tool_can_use/get_knowledge.py b/src/do_tool/tool_can_use/get_knowledge.py index bd4ce86b..20a92264 100644 --- a/src/do_tool/tool_can_use/get_knowledge.py +++ b/src/do_tool/tool_can_use/get_knowledge.py @@ -2,7 +2,7 @@ from src.do_tool.tool_can_use.base_tool import BaseTool from src.plugins.chat.utils import get_embedding from src.common.database import db from src.common.logger_manager import get_logger -from typing import Dict, Any, Union +from typing import Any, Union logger = get_logger("get_knowledge_tool") @@ -21,15 +21,14 @@ class SearchKnowledgeTool(BaseTool): "required": ["query"], } - async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]: + async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]: """执行知识库搜索 Args: function_args: 工具参数 - message_txt: 原始消息文本 Returns: - Dict: 工具执行结果 + dict: 工具执行结果 """ try: query = function_args.get("query") diff --git a/src/do_tool/tool_can_use/get_memory.py b/src/do_tool/tool_can_use/get_memory.py index b38423ed..c1bcc927 100644 --- a/src/do_tool/tool_can_use/get_memory.py +++ b/src/do_tool/tool_can_use/get_memory.py @@ -25,7 +25,6 @@ class GetMemoryTool(BaseTool): Args: function_args: 工具参数 - message_txt: 原始消息文本 Returns: Dict: 工具执行结果 diff --git a/src/do_tool/tool_can_use/get_time_date.py b/src/do_tool/tool_can_use/get_time_date.py index 6104026e..bd3b1a9c 100644 --- a/src/do_tool/tool_can_use/get_time_date.py +++ b/src/do_tool/tool_can_use/get_time_date.py @@ -22,7 +22,6 @@ class GetCurrentDateTimeTool(BaseTool): Args: function_args: 工具参数(此工具不使用) - message_txt: 原始消息文本(此工具不使用) Returns: Dict: 工具执行结果 diff --git a/src/do_tool/tool_can_use/lpmm_get_knowledge.py b/src/do_tool/tool_can_use/lpmm_get_knowledge.py index 4dba1bc7..8754e603 100644 --- a/src/do_tool/tool_can_use/lpmm_get_knowledge.py +++ b/src/do_tool/tool_can_use/lpmm_get_knowledge.py @@ -29,7 +29,6 @@ class SearchKnowledgeFromLPMMTool(BaseTool): Args: function_args: 工具参数 - message_txt: 原始消息文本 Returns: Dict: 工具执行结果 diff --git a/src/do_tool/tool_use.py b/src/do_tool/tool_use.py index 88289fe0..b2f59cc8 100644 --- a/src/do_tool/tool_use.py +++ b/src/do_tool/tool_use.py @@ -106,7 +106,6 @@ class ToolUser: Args: message_txt: 用户消息文本 - sender_name: 发送者名称 chat_stream: 聊天流对象 observation: 观察对象(可选) diff --git a/src/individuality/scene.py b/src/individuality/scene.py index 76304dbb..4edd5dc2 100644 --- a/src/individuality/scene.py +++ b/src/individuality/scene.py @@ -1,9 +1,9 @@ import json -from typing import Dict import os +from typing import Any -def load_scenes() -> Dict: +def load_scenes() -> dict[str, Any]: """ 从JSON文件加载场景数据 @@ -20,7 +20,7 @@ def load_scenes() -> Dict: PERSONALITY_SCENES = load_scenes() -def get_scene_by_factor(factor: str) -> Dict: +def get_scene_by_factor(factor: str) -> dict | None: """ 根据人格因子获取对应的情景测试 @@ -28,12 +28,12 @@ def get_scene_by_factor(factor: str) -> Dict: factor (str): 人格因子名称 Returns: - Dict: 包含情景描述的字典 + dict: 包含情景描述的字典 """ - return PERSONALITY_SCENES.get(factor, None) + return PERSONALITY_SCENES.get(factor,None) -def get_all_scenes() -> Dict: +def get_all_scenes() -> dict: """ 获取所有情景测试 diff --git a/src/plugins/PFC/chat_observer.py b/src/plugins/PFC/chat_observer.py index 2822e111..e99ed800 100644 --- a/src/plugins/PFC/chat_observer.py +++ b/src/plugins/PFC/chat_observer.py @@ -23,6 +23,7 @@ class ChatObserver: Args: stream_id: 聊天流ID + private_name: 私聊名称 Returns: ChatObserver: 观察器实例 diff --git a/src/plugins/PFC/pfc_manager.py b/src/plugins/PFC/pfc_manager.py index 621686a9..7837606c 100644 --- a/src/plugins/PFC/pfc_manager.py +++ b/src/plugins/PFC/pfc_manager.py @@ -33,6 +33,7 @@ class PFCManager: Args: stream_id: 聊天流ID + private_name: 私聊名称 Returns: Optional[Conversation]: 对话实例,创建失败则返回None diff --git a/src/plugins/PFC/pfc_utils.py b/src/plugins/PFC/pfc_utils.py index 5e35d47b..2f7bd5e0 100644 --- a/src/plugins/PFC/pfc_utils.py +++ b/src/plugins/PFC/pfc_utils.py @@ -18,6 +18,7 @@ def get_items_from_json( Args: content: 包含JSON的文本 + private_name: 私聊名称 *items: 要提取的字段名 default_values: 字段的默认值,格式为 {字段名: 默认值} required_types: 字段的必需类型,格式为 {字段名: 类型} diff --git a/src/plugins/PFC/reply_checker.py b/src/plugins/PFC/reply_checker.py index 18088895..35e9af50 100644 --- a/src/plugins/PFC/reply_checker.py +++ b/src/plugins/PFC/reply_checker.py @@ -29,6 +29,8 @@ class ReplyChecker: Args: reply: 生成的回复 goal: 对话目标 + chat_history: 对话历史记录 + chat_history_text: 对话历史记录文本 retry_count: 当前重试次数 Returns: diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py index 525d30c9..2de88561 100644 --- a/src/plugins/chat/message.py +++ b/src/plugins/chat/message.py @@ -1,6 +1,7 @@ import time +from abc import abstractmethod from dataclasses import dataclass -from typing import Dict, List, Optional, Union +from typing import Optional, Any import urllib3 @@ -58,12 +59,37 @@ class Message(MessageBase): # 回复消息 self.reply = reply + async def _process_message_segments(self, segment: Seg) -> str: + """递归处理消息段,转换为文字描述 + + Args: + segment: 要处理的消息段 + + Returns: + str: 处理后的文本 + """ + if segment.type == "seglist": + # 处理消息段列表 + segments_text = [] + for seg in segment.data: + processed = await self._process_message_segments(seg) + if processed: + segments_text.append(processed) + return " ".join(segments_text) + else: + # 处理单个消息段 + return await self._process_single_segment(segment) + + @abstractmethod + async def _process_single_segment(self, segment): + pass + @dataclass class MessageRecv(Message): """接收消息类,用于处理从MessageCQ序列化的消息""" - def __init__(self, message_dict: Dict): + def __init__(self, message_dict: dict[str, Any]): """从MessageCQ的字典初始化 Args: @@ -90,26 +116,7 @@ class MessageRecv(Message): self.processed_plain_text = await self._process_message_segments(self.message_segment) self.detailed_plain_text = self._generate_detailed_text() - async def _process_message_segments(self, segment: Seg) -> str: - """递归处理消息段,转换为文字描述 - Args: - segment: 要处理的消息段 - - Returns: - str: 处理后的文本 - """ - if segment.type == "seglist": - # 处理消息段列表 - segments_text = [] - for seg in segment.data: - processed = await self._process_message_segments(seg) - if processed: - segments_text.append(processed) - return " ".join(segments_text) - else: - # 处理单个消息段 - return await self._process_single_segment(segment) async def _process_single_segment(self, seg: Seg) -> str: """处理单个消息段 @@ -179,28 +186,7 @@ class MessageProcessBase(Message): self.thinking_time = round(time.time() - self.thinking_start_time, 2) return self.thinking_time - async def _process_message_segments(self, segment: Seg) -> str: - """递归处理消息段,转换为文字描述 - - Args: - segment: 要处理的消息段 - - Returns: - str: 处理后的文本 - """ - if segment.type == "seglist": - # 处理消息段列表 - segments_text = [] - for seg in segment.data: - processed = await self._process_message_segments(seg) - if processed: - segments_text.append(processed) - return " ".join(segments_text) - else: - # 处理单个消息段 - return await self._process_single_segment(segment) - - async def _process_single_segment(self, seg: Seg) -> Union[str, None]: + async def _process_single_segment(self, seg: Seg) -> str | None: """处理单个消息段 Args: @@ -278,7 +264,7 @@ class MessageSending(MessageProcessBase): message_id: str, chat_stream: ChatStream, bot_user_info: UserInfo, - sender_info: UserInfo, # 用来记录发送者信息,用于私聊回复 + sender_info: UserInfo | None, # 用来记录发送者信息,用于私聊回复 message_segment: Seg, reply: Optional["MessageRecv"] = None, is_head: bool = False, @@ -303,7 +289,7 @@ class MessageSending(MessageProcessBase): self.is_emoji = is_emoji self.apply_set_reply_logic = apply_set_reply_logic - def set_reply(self, reply: Optional["MessageRecv"] = None) -> None: + def set_reply(self, reply: Optional["MessageRecv"] = None): """设置回复消息""" if self.message_info.format_info is not None and "reply" in self.message_info.format_info.accept_format: if reply: @@ -317,7 +303,6 @@ class MessageSending(MessageProcessBase): self.message_segment, ], ) - return self async def process(self) -> None: """处理消息内容,生成纯文本和详细文本""" @@ -342,6 +327,7 @@ class MessageSending(MessageProcessBase): reply=thinking.reply, is_head=is_head, is_emoji=is_emoji, + sender_info=None, ) def to_dict(self): @@ -361,7 +347,7 @@ class MessageSet: def __init__(self, chat_stream: ChatStream, message_id: str): self.chat_stream = chat_stream self.message_id = message_id - self.messages: List[MessageSending] = [] + self.messages: list[MessageSending] = [] self.time = round(time.time(), 3) # 保留3位小数 def add_message(self, message: MessageSending) -> None: diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index 30d943d9..ee4865d9 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -1,7 +1,7 @@ # src/plugins/chat/message_sender.py import asyncio import time -from typing import Dict, List, Optional, Union +from typing import Union # from ...common.database import db # 数据库依赖似乎不需要了,注释掉 from ..message.api import global_api @@ -70,7 +70,7 @@ class MessageContainer: def __init__(self, chat_id: str, max_size: int = 100): self.chat_id = chat_id self.max_size = max_size - self.messages: List[Union[MessageThinking, MessageSending]] = [] # 明确类型 + self.messages: list[MessageThinking | MessageSending] = [] # 明确类型 self.last_send_time = 0 self.thinking_wait_timeout = 20 # 思考等待超时时间(秒) - 从旧 sender 合并 @@ -78,7 +78,7 @@ class MessageContainer: """计算当前容器中思考消息的数量""" return sum(1 for msg in self.messages if isinstance(msg, MessageThinking)) - def get_timeout_sending_messages(self) -> List[MessageSending]: + def get_timeout_sending_messages(self) -> list[MessageSending]: """获取所有超时的MessageSending对象(思考时间超过20秒),按thinking_start_time排序 - 从旧 sender 合并""" current_time = time.time() timeout_messages = [] @@ -94,7 +94,7 @@ class MessageContainer: timeout_messages.sort(key=lambda x: x.thinking_start_time) return timeout_messages - def get_earliest_message(self) -> Optional[Union[MessageThinking, MessageSending]]: + def get_earliest_message(self): """获取thinking_start_time最早的消息对象""" if not self.messages: return None @@ -108,7 +108,7 @@ class MessageContainer: earliest_message = msg return earliest_message - def add_message(self, message: Union[MessageThinking, MessageSending, MessageSet]) -> None: + def add_message(self, message: Union[MessageThinking, MessageSending, MessageSet]): """添加消息到队列""" if isinstance(message, MessageSet): for single_message in message.messages: @@ -116,7 +116,7 @@ class MessageContainer: else: self.messages.append(message) - def remove_message(self, message_to_remove: Union[MessageThinking, MessageSending]) -> bool: + def remove_message(self, message_to_remove: Union[MessageThinking, MessageSending]): """移除指定的消息对象,如果消息存在则返回True,否则返回False""" try: _initial_len = len(self.messages) @@ -138,7 +138,7 @@ class MessageContainer: """检查是否有待发送的消息""" return bool(self.messages) - def get_all_messages(self) -> List[Union[MessageSending, MessageThinking]]: + def get_all_messages(self) -> list[MessageThinking | MessageSending]: """获取所有消息""" return list(self.messages) # 返回副本 @@ -148,7 +148,7 @@ class MessageManager: def __init__(self): self._processor_task = None - self.containers: Dict[str, MessageContainer] = {} + self.containers: dict[str, MessageContainer] = {} self.storage = MessageStorage() # 添加 storage 实例 self._running = True # 处理器运行状态 self._container_lock = asyncio.Lock() # 保护 containers 字典的锁 diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index 16581f3a..40b4ab7c 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -2,7 +2,6 @@ import random import time import re from collections import Counter -from typing import Dict, List, Optional import jieba import numpy as np @@ -26,7 +25,7 @@ def is_english_letter(char: str) -> bool: return "a" <= char.lower() <= "z" -def db_message_to_str(message_dict: Dict) -> str: +def db_message_to_str(message_dict: dict) -> str: logger.debug(f"message_dict: {message_dict}") time_str = time.strftime("%m-%d %H:%M:%S", time.localtime(message_dict["time"])) try: @@ -35,7 +34,7 @@ def db_message_to_str(message_dict: Dict) -> str: message_dict.get("user_nickname", ""), message_dict.get("user_cardname", ""), ) - except Exception: + except Exception as e: name = message_dict.get("user_nickname", "") or f"用户{message_dict['user_id']}" content = message_dict.get("processed_plain_text", "") result = f"[{time_str}] {name}: {content}\n" @@ -77,13 +76,13 @@ def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, float]: if not is_mentioned: # 判断是否被回复 if re.match( - f"\[回复 [\s\S]*?\({str(global_config.BOT_QQ)}\):[\s\S]*?\],说:", message.processed_plain_text + f"\[回复 [\s\S]*?\({str(global_config.BOT_QQ)}\):[\s\S]*?],说:" , message.processed_plain_text ): is_mentioned = True else: # 判断内容中是否被提及 message_content = re.sub(r"@[\s\S]*?((\d+))", "", message.processed_plain_text) - message_content = re.sub(r"\[回复 [\s\S]*?\(((\d+)|未知id)\):[\s\S]*?\],说:", "", message_content) + message_content = re.sub(r"\[回复 [\s\S]*?\(((\d+)|未知id)\):[\s\S]*?],说:", "", message_content) for keyword in keywords: if keyword in message_content: is_mentioned = True @@ -223,7 +222,7 @@ def get_recent_group_speaker(chat_stream_id: int, sender, limit: int = 12) -> li return who_chat_in_group -def split_into_sentences_w_remove_punctuation(text: str) -> List[str]: +def split_into_sentences_w_remove_punctuation(text: str) -> list[str]: """将文本分割成句子,并根据概率合并 1. 识别分割点(, , 。 ; 空格),但如果分割点左右都是英文字母则不分割。 2. 将文本分割成 (内容, 分隔符) 的元组。 @@ -370,7 +369,7 @@ def random_remove_punctuation(text: str) -> str: return result -def process_llm_response(text: str) -> List[str]: +def process_llm_response(text: str) -> list[str]: # 先保护颜文字 if global_config.enable_kaomoji_protection: protected_text, kaomoji_mapping = protect_kaomoji(text) @@ -379,7 +378,7 @@ def process_llm_response(text: str) -> List[str]: protected_text = text kaomoji_mapping = {} # 提取被 () 或 [] 包裹且包含中文的内容 - pattern = re.compile(r"[\(\[\(](?=.*[\u4e00-\u9fff]).*?[\)\]\)]") + pattern = re.compile(r"[(\[(](?=.*[一-鿿]).*?[)\])]") # _extracted_contents = pattern.findall(text) _extracted_contents = pattern.findall(protected_text) # 在保护后的文本上查找 # 去除 () 和 [] 及其包裹的内容 @@ -554,7 +553,7 @@ def protect_kaomoji(sentence): r"[^()\[\]()【】]*?" # 非括号字符(惰性匹配) r"[^一-龥a-zA-Z0-9\s]" # 非中文、非英文、非数字、非空格字符(必须包含至少一个) r"[^()\[\]()【】]*?" # 非括号字符(惰性匹配) - r"[\)\])】" # 右括号 + r"[)\])】" # 右括号 r"]" r")" r"|" @@ -704,7 +703,7 @@ def count_messages_between(start_time: float, end_time: float, stream_id: str) - return 0, 0 -def translate_timestamp_to_human_readable(timestamp: float, mode: str = "normal") -> Optional[str]: +def translate_timestamp_to_human_readable(timestamp: float, mode: str = "normal") -> str: """将时间戳转换为人类可读的时间格式 Args: @@ -732,10 +731,9 @@ def translate_timestamp_to_human_readable(timestamp: float, mode: str = "normal" return f"{int(diff / 86400)}天前:\n" else: return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp)) + ":\n" - elif mode == "lite": + else: # mode = "lite" or unknown # 只返回时分秒格式,喵~ return time.strftime("%H:%M:%S", time.localtime(timestamp)) - return None def parse_text_timestamps(text: str, mode: str = "normal") -> str: diff --git a/src/plugins/memory_system/Hippocampus.py b/src/plugins/memory_system/Hippocampus.py index 7a5fc1a8..ff34541e 100644 --- a/src/plugins/memory_system/Hippocampus.py +++ b/src/plugins/memory_system/Hippocampus.py @@ -511,7 +511,7 @@ class Hippocampus: """从文本中提取关键词并获取相关记忆。 Args: - topic (str): 记忆主题 + keywords (list): 输入文本 max_memory_num (int, optional): 返回的记忆条目数量上限。默认为3,表示最多返回3条与输入文本相关度最高的记忆。 max_memory_length (int, optional): 每个主题最多返回的记忆条目数量。默认为2,表示每个主题最多返回2条相似度最高的记忆。 max_depth (int, optional): 记忆检索深度。默认为3。值越大,检索范围越广,可以获取更多间接相关的记忆,但速度会变慢。 @@ -829,7 +829,7 @@ class EntorhinalCortex: return chat_samples @staticmethod - def random_get_msg_snippet(target_timestamp: float, chat_size: int, max_memorized_time_per_msg: int) -> list: + def random_get_msg_snippet(target_timestamp: float, chat_size: int, max_memorized_time_per_msg: int) -> list | None: """从数据库中随机获取指定时间戳附近的消息片段 (使用 chat_message_builder)""" try_count = 0 time_window_seconds = random.randint(300, 1800) # 随机时间窗口,5到30分钟 diff --git a/src/plugins/schedule/schedule_generator.py b/src/plugins/schedule/schedule_generator.py index ee7bdee1..6bd2e587 100644 --- a/src/plugins/schedule/schedule_generator.py +++ b/src/plugins/schedule/schedule_generator.py @@ -1,7 +1,6 @@ import datetime import os import sys -from typing import Dict import asyncio from dateutil import tz @@ -162,7 +161,7 @@ class ScheduleGenerator: async def generate_daily_schedule( self, target_date: datetime.datetime = None, - ) -> Dict[str, str]: + ) -> dict[str, str]: daytime_prompt = self.construct_daytime_prompt(target_date) daytime_response, _ = await self.llm_scheduler_all.generate_response_async(daytime_prompt) return daytime_response From e4959f0386b851f6b40e489672143ed6f053d9cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E7=8C=AB?= Date: Thu, 1 May 2025 07:07:13 +0900 Subject: [PATCH 10/67] ruff fix --- src/common/message_repository.py | 2 +- src/do_tool/not_used/change_relationship.py | 2 +- src/do_tool/not_used/send_emoji.py | 2 +- src/do_tool/tool_can_use/base_tool.py | 2 +- src/heart_flow/background_tasks.py | 10 +++----- src/heart_flow/sub_mind.py | 2 +- src/individuality/scene.py | 2 +- src/plugins/chat/message.py | 2 -- src/plugins/chat/message_sender.py | 3 +-- src/plugins/chat/utils.py | 6 ++--- src/plugins/heartFC_chat/heartFC_sender.py | 1 - .../heartFC_chat/heartflow_prompt_builder.py | 4 +--- .../respon_info_catcher/info_catcher.py | 23 ++++++++++++------- 13 files changed, 29 insertions(+), 32 deletions(-) diff --git a/src/common/message_repository.py b/src/common/message_repository.py index 11bd6095..9f246077 100644 --- a/src/common/message_repository.py +++ b/src/common/message_repository.py @@ -1,7 +1,7 @@ from src.common.database import db from src.common.logger import get_module_logger import traceback -from typing import List, Dict, Any, Optional +from typing import List, Any, Optional logger = get_module_logger(__name__) diff --git a/src/do_tool/not_used/change_relationship.py b/src/do_tool/not_used/change_relationship.py index ab91489f..96f512e5 100644 --- a/src/do_tool/not_used/change_relationship.py +++ b/src/do_tool/not_used/change_relationship.py @@ -1,4 +1,4 @@ -from typing import Dict, Any +from typing import Any from src.common.logger_manager import get_logger from src.do_tool.tool_can_use.base_tool import BaseTool diff --git a/src/do_tool/not_used/send_emoji.py b/src/do_tool/not_used/send_emoji.py index 9bf2dd48..d2d00a92 100644 --- a/src/do_tool/not_used/send_emoji.py +++ b/src/do_tool/not_used/send_emoji.py @@ -1,7 +1,7 @@ from src.do_tool.tool_can_use.base_tool import BaseTool from src.common.logger import get_module_logger -from typing import Dict, Any +from typing import Any logger = get_module_logger("send_emoji_tool") diff --git a/src/do_tool/tool_can_use/base_tool.py b/src/do_tool/tool_can_use/base_tool.py index b01e543e..42570884 100644 --- a/src/do_tool/tool_can_use/base_tool.py +++ b/src/do_tool/tool_can_use/base_tool.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Any, Optional, Type +from typing import List, Any, Optional, Type import inspect import importlib import pkgutil diff --git a/src/heart_flow/background_tasks.py b/src/heart_flow/background_tasks.py index 38360653..1b64c205 100644 --- a/src/heart_flow/background_tasks.py +++ b/src/heart_flow/background_tasks.py @@ -26,7 +26,7 @@ LOG_INTERVAL_SECONDS = 3 async def _run_periodic_loop( - task_name: str, interval: int, task_func: Callable[..., Coroutine[Any, Any, None]], **kwargs + task_name: str, interval: int, task_func: Callable[..., Coroutine[Any, Any, None]], **kwargs ): """周期性任务主循环""" while True: @@ -250,14 +250,10 @@ class BackgroundTaskManager: # --- Specific Task Runners --- # async def _run_state_update_cycle(self, interval: int): - await _run_periodic_loop( - task_name="State Update", interval=interval, task_func=self._perform_state_update_work - ) + await _run_periodic_loop(task_name="State Update", interval=interval, task_func=self._perform_state_update_work) async def _run_absent_into_chat(self, interval: int): - await _run_periodic_loop( - task_name="Into Chat", interval=interval, task_func=self._perform_absent_into_chat - ) + await _run_periodic_loop(task_name="Into Chat", interval=interval, task_func=self._perform_absent_into_chat) async def _run_normal_chat_timeout_check_cycle(self, interval: int): await _run_periodic_loop( diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index 09d7b936..f414e6b2 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -78,7 +78,7 @@ def calculate_replacement_probability(similarity: float) -> float: # p = 3.5 * s - 1.4 probability = 3.5 * similarity - 1.4 return max(0.0, probability) - else: # 0.6 < similarity < 0.9 + else: # 0.6 < similarity < 0.9 # p = s + 0.1 probability = similarity + 0.1 return min(1.0, max(0.0, probability)) diff --git a/src/individuality/scene.py b/src/individuality/scene.py index 4edd5dc2..8d7af97f 100644 --- a/src/individuality/scene.py +++ b/src/individuality/scene.py @@ -30,7 +30,7 @@ def get_scene_by_factor(factor: str) -> dict | None: Returns: dict: 包含情景描述的字典 """ - return PERSONALITY_SCENES.get(factor,None) + return PERSONALITY_SCENES.get(factor, None) def get_all_scenes() -> dict: diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py index 2de88561..e753a999 100644 --- a/src/plugins/chat/message.py +++ b/src/plugins/chat/message.py @@ -116,8 +116,6 @@ class MessageRecv(Message): self.processed_plain_text = await self._process_message_segments(self.message_segment) self.detailed_plain_text = self._generate_detailed_text() - - async def _process_single_segment(self, seg: Seg) -> str: """处理单个消息段 diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index ee4865d9..8d134847 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -4,7 +4,6 @@ import time from typing import Union # from ...common.database import db # 数据库依赖似乎不需要了,注释掉 -from ..message.api import global_api from .message import MessageSending, MessageThinking, MessageSet from ..storage.storage import MessageStorage @@ -27,7 +26,7 @@ async def send_via_ws(message: MessageSending) -> None: async def send_message( - message: MessageSending, + message: MessageSending, ) -> None: """发送消息(核心发送逻辑)""" diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index 40b4ab7c..a7408681 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -34,7 +34,7 @@ def db_message_to_str(message_dict: dict) -> str: message_dict.get("user_nickname", ""), message_dict.get("user_cardname", ""), ) - except Exception as e: + except Exception: name = message_dict.get("user_nickname", "") or f"用户{message_dict['user_id']}" content = message_dict.get("processed_plain_text", "") result = f"[{time_str}] {name}: {content}\n" @@ -76,7 +76,7 @@ def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, float]: if not is_mentioned: # 判断是否被回复 if re.match( - f"\[回复 [\s\S]*?\({str(global_config.BOT_QQ)}\):[\s\S]*?],说:" , message.processed_plain_text + f"\[回复 [\s\S]*?\({str(global_config.BOT_QQ)}\):[\s\S]*?],说:", message.processed_plain_text ): is_mentioned = True else: @@ -731,7 +731,7 @@ def translate_timestamp_to_human_readable(timestamp: float, mode: str = "normal" return f"{int(diff / 86400)}天前:\n" else: return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp)) + ":\n" - else: # mode = "lite" or unknown + else: # mode = "lite" or unknown # 只返回时分秒格式,喵~ return time.strftime("%H:%M:%S", time.localtime(timestamp)) diff --git a/src/plugins/heartFC_chat/heartFC_sender.py b/src/plugins/heartFC_chat/heartFC_sender.py index a4103667..fc96207d 100644 --- a/src/plugins/heartFC_chat/heartFC_sender.py +++ b/src/plugins/heartFC_chat/heartFC_sender.py @@ -1,7 +1,6 @@ # src/plugins/heartFC_chat/heartFC_sender.py import asyncio # 重新导入 asyncio from typing import Dict, Optional # 重新导入类型 -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 diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 9b83f265..c6cdf251 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -151,9 +151,7 @@ JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query": Prompt("\n你有以下这些**知识**:\n{prompt_info}\n请你**记住上面的知识**,之后可能会用到。\n", "knowledge_prompt") -async def _build_prompt_focus( - reason, current_mind_info, structured_info, chat_stream, sender_name -) -> tuple[str, str]: +async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_stream, sender_name) -> tuple[str, str]: individuality = Individuality.get_instance() prompt_personality = individuality.get_prompt(x_person=0, level=2) # 日程构建 diff --git a/src/plugins/respon_info_catcher/info_catcher.py b/src/plugins/respon_info_catcher/info_catcher.py index 08be4a76..32add842 100644 --- a/src/plugins/respon_info_catcher/info_catcher.py +++ b/src/plugins/respon_info_catcher/info_catcher.py @@ -185,18 +185,25 @@ class InfoCatcher: try: # 将消息对象转换为可序列化的字典喵~ - thinking_log_data = {"chat_id": self.chat_id, "trigger_text": self.trigger_response_text, - "response_text": self.response_text, "trigger_info": { + thinking_log_data = { + "chat_id": self.chat_id, + "trigger_text": self.trigger_response_text, + "response_text": self.response_text, + "trigger_info": { "time": self.trigger_response_time, "message": self.message_to_dict(self.trigger_response_message), - }, "response_info": { + }, + "response_info": { "time": self.response_time, "message": self.response_messages, - }, "timing_results": self.timing_results, "chat_history": self.message_list_to_dict(self.chat_history), - "chat_history_in_thinking": self.message_list_to_dict(self.chat_history_in_thinking), - "chat_history_after_response": self.message_list_to_dict( - self.chat_history_after_response), "heartflow_data": self.heartflow_data, - "reasoning_data": self.reasoning_data} + }, + "timing_results": self.timing_results, + "chat_history": self.message_list_to_dict(self.chat_history), + "chat_history_in_thinking": self.message_list_to_dict(self.chat_history_in_thinking), + "chat_history_after_response": self.message_list_to_dict(self.chat_history_after_response), + "heartflow_data": self.heartflow_data, + "reasoning_data": self.reasoning_data, + } # 根据不同的响应模式添加相应的数据喵~ # 现在直接都加上去好了喵~ # if self.response_mode == "heart_flow": From 45c64208b40c39e969658746aaac734d9d51a483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E7=8C=AB?= Date: Thu, 1 May 2025 07:24:52 +0900 Subject: [PATCH 11/67] refactor: Clean up unused variables and improve code readability --- bot.py | 2 -- scripts/info_extraction.py | 2 +- src/common/message_repository.py | 1 - src/individuality/individuality.py | 1 - src/plugins/PFC/action_planner.py | 1 - src/plugins/PFC/chat_observer.py | 6 +----- src/plugins/chat/utils.py | 2 +- src/plugins/emoji_system/emoji_manager.py | 2 -- src/plugins/heartFC_chat/heartFC_chat.py | 9 ++++----- src/plugins/memory_system/Hippocampus.py | 2 -- src/plugins/person_info/person_info.py | 1 - src/plugins/willing/mode_dynamic.py | 3 --- src/plugins/willing/mode_llmcheck.py | 6 ++---- 13 files changed, 9 insertions(+), 29 deletions(-) diff --git a/bot.py b/bot.py index 770f5365..9a4a5000 100644 --- a/bot.py +++ b/bot.py @@ -130,9 +130,7 @@ def check_eula(): privacy_file = Path("PRIVACY.md") eula_updated = True - eula_new_hash = None privacy_updated = True - privacy_new_hash = None eula_confirmed = False privacy_confirmed = False diff --git a/scripts/info_extraction.py b/scripts/info_extraction.py index fdb44528..65c4082b 100644 --- a/scripts/info_extraction.py +++ b/scripts/info_extraction.py @@ -76,7 +76,7 @@ def process_single_text(pg_hash, raw_data, llm_client_list): return doc_item, None -def signal_handler(signum, frame): +def signal_handler(_signum, _frame): """处理Ctrl+C信号""" logger.info("\n接收到中断信号,正在优雅地关闭程序...") shutdown_event.set() diff --git a/src/common/message_repository.py b/src/common/message_repository.py index 9f246077..03f192ce 100644 --- a/src/common/message_repository.py +++ b/src/common/message_repository.py @@ -26,7 +26,6 @@ def find_messages( """ try: query = db.messages.find(message_filter) - results: List[dict[str, Any]] = [] if limit > 0: if limit_mode == "earliest": diff --git a/src/individuality/individuality.py b/src/individuality/individuality.py index 86e5b63e..9ffdfdaa 100644 --- a/src/individuality/individuality.py +++ b/src/individuality/individuality.py @@ -113,7 +113,6 @@ class Individuality: p_pronoun = "我" prompt_personality = f"{p_pronoun}{self.personality.personality_core}" else: # x_person == 0 - p_pronoun = "" # 无人称 # 对于无人称,直接描述核心特征 prompt_personality = f"{self.personality.personality_core}" diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 23de9f0d..4770c6ce 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -262,7 +262,6 @@ class ActionPlanner: # --- 知识信息字符串构建结束 --- # 获取聊天历史记录 (chat_history_text) - chat_history_text = "" try: if hasattr(observation_info, "chat_history") and observation_info.chat_history: chat_history_text = observation_info.chat_history_str diff --git a/src/plugins/PFC/chat_observer.py b/src/plugins/PFC/chat_observer.py index e99ed800..3576397d 100644 --- a/src/plugins/PFC/chat_observer.py +++ b/src/plugins/PFC/chat_observer.py @@ -123,11 +123,7 @@ class ChatObserver: self.last_cold_chat_check = current_time # 判断是否冷场 - is_cold = False - if self.last_message_time is None: - is_cold = True - else: - is_cold = (current_time - self.last_message_time) > self.cold_chat_threshold + is_cold = True if self.last_message_time is None else (current_time - self.last_message_time) > self.cold_chat_threshold # 如果冷场状态发生变化,发送通知 if is_cold != self.is_cold_chat_state: diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index a7408681..cdaa2194 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -156,7 +156,7 @@ async def get_recent_group_messages(chat_id: str, limit: int = 12) -> list: return message_objects -def get_recent_group_detailed_plain_text(chat_stream_id: int, limit: int = 12, combine=False): +def get_recent_group_detailed_plain_text(chat_stream_id: str, limit: int = 12, combine=False): recent_messages = list( db.messages.find( {"chat_id": chat_stream_id}, diff --git a/src/plugins/emoji_system/emoji_manager.py b/src/plugins/emoji_system/emoji_manager.py index a75a4f90..f789f906 100644 --- a/src/plugins/emoji_system/emoji_manager.py +++ b/src/plugins/emoji_system/emoji_manager.py @@ -50,8 +50,6 @@ class MaiEmoji: async def initialize_hash_format(self): """从文件创建表情包实例, 计算哈希值和格式""" - image_base64 = None - image_bytes = None try: # 使用 full_path 检查文件是否存在 if not os.path.exists(self.full_path): diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 47d420dd..71b8d81c 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -174,7 +174,7 @@ class HeartFChatting: self, chat_id: str, sub_mind: SubMind, - observations: Observation, + observations: list[Observation], on_consecutive_no_reply_callback: Callable[[], Coroutine[None, None, None]], ): """ @@ -631,19 +631,18 @@ class HeartFChatting: observation = self.observations[0] if self.observations else None try: - dang_qian_deng_dai = 0.0 # 初始化本次等待时间 with Timer("等待新消息", cycle_timers): # 等待新消息、超时或关闭信号,并获取结果 await self._wait_for_new_message(observation, planner_start_db_time, self.log_prefix) # 从计时器获取实际等待时间 - dang_qian_deng_dai = cycle_timers.get("等待新消息", 0.0) + current_waiting = cycle_timers.get("等待新消息", 0.0) if not self._shutting_down: self._lian_xu_bu_hui_fu_ci_shu += 1 - self._lian_xu_deng_dai_shi_jian += dang_qian_deng_dai # 累加等待时间 + self._lian_xu_deng_dai_shi_jian += current_waiting # 累加等待时间 logger.debug( f"{self.log_prefix} 连续不回复计数增加: {self._lian_xu_bu_hui_fu_ci_shu}/{CONSECUTIVE_NO_REPLY_THRESHOLD}, " - f"本次等待: {dang_qian_deng_dai:.2f}秒, 累计等待: {self._lian_xu_deng_dai_shi_jian:.2f}秒" + f"本次等待: {current_waiting:.2f}秒, 累计等待: {self._lian_xu_deng_dai_shi_jian:.2f}秒" ) # 检查是否同时达到次数和时间阈值 diff --git a/src/plugins/memory_system/Hippocampus.py b/src/plugins/memory_system/Hippocampus.py index ff34541e..ccc4d4fa 100644 --- a/src/plugins/memory_system/Hippocampus.py +++ b/src/plugins/memory_system/Hippocampus.py @@ -364,7 +364,6 @@ class Hippocampus: logger.debug(f"有效的关键词: {', '.join(valid_keywords)}") # 从每个关键词获取记忆 - all_memories = [] activate_map = {} # 存储每个词的累计激活值 # 对每个关键词进行扩散式检索 @@ -536,7 +535,6 @@ class Hippocampus: logger.debug(f"有效的关键词: {', '.join(valid_keywords)}") # 从每个关键词获取记忆 - all_memories = [] activate_map = {} # 存储每个词的累计激活值 # 对每个关键词进行扩散式检索 diff --git a/src/plugins/person_info/person_info.py b/src/plugins/person_info/person_info.py index 8bafe5eb..a71f95f8 100644 --- a/src/plugins/person_info/person_info.py +++ b/src/plugins/person_info/person_info.py @@ -137,7 +137,6 @@ class PersonInfoManager: @staticmethod def _extract_json_from_text(text: str) -> dict: """从文本中提取JSON数据的高容错方法""" - parsed_json = None try: # 尝试直接解析 parsed_json = json.loads(text) diff --git a/src/plugins/willing/mode_dynamic.py b/src/plugins/willing/mode_dynamic.py index ab1389ea..029da4e0 100644 --- a/src/plugins/willing/mode_dynamic.py +++ b/src/plugins/willing/mode_dynamic.py @@ -50,7 +50,6 @@ class DynamicWillingManager(BaseWillingManager): is_high_mode = self.chat_high_willing_mode.get(chat_id, False) # 获取当前模式的持续时间 - duration = 0 if is_high_mode: duration = self.chat_high_willing_duration.get(chat_id, 180) # 默认3分钟 else: @@ -154,8 +153,6 @@ class DynamicWillingManager(BaseWillingManager): ) # 根据当前模式计算回复概率 - base_probability = 0.0 - if in_conversation_context: # 在对话上下文中,降低基础回复概率 base_probability = 0.5 if is_high_mode else 0.25 diff --git a/src/plugins/willing/mode_llmcheck.py b/src/plugins/willing/mode_llmcheck.py index ec1cde29..697621b1 100644 --- a/src/plugins/willing/mode_llmcheck.py +++ b/src/plugins/willing/mode_llmcheck.py @@ -76,10 +76,8 @@ class LlmcheckWillingManager(MxpWillingManager): current_date = time.strftime("%Y-%m-%d", time.localtime()) current_time = time.strftime("%H:%M:%S", time.localtime()) - chat_talking_prompt = "" - if chat_id: - chat_talking_prompt = get_recent_group_detailed_plain_text(chat_id, limit=length, combine=True) - else: + chat_talking_prompt = get_recent_group_detailed_plain_text(chat_id, limit=length, combine=True) + if not chat_id: return 0 # if is_mentioned_bot: From 0fba84c193d8911a48f5a5eaa9d591948d7b290c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 30 Apr 2025 22:25:33 +0000 Subject: [PATCH 12/67] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/chat_observer.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/plugins/PFC/chat_observer.py b/src/plugins/PFC/chat_observer.py index 3576397d..e66824a2 100644 --- a/src/plugins/PFC/chat_observer.py +++ b/src/plugins/PFC/chat_observer.py @@ -123,7 +123,11 @@ class ChatObserver: self.last_cold_chat_check = current_time # 判断是否冷场 - is_cold = True if self.last_message_time is None else (current_time - self.last_message_time) > self.cold_chat_threshold + is_cold = ( + True + if self.last_message_time is None + else (current_time - self.last_message_time) > self.cold_chat_threshold + ) # 如果冷场状态发生变化,发送通知 if is_cold != self.is_cold_chat_state: From 1b30598797a51812c455eb549c339769e27ce0cd Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Thu, 1 May 2025 09:59:36 +0800 Subject: [PATCH 13/67] =?UTF-8?q?=E8=AE=A9=E9=BA=A6=E9=BA=A6=E8=87=B3?= =?UTF-8?q?=E5=B0=91=E8=83=BD=E5=90=AF=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/message_sender.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index 8d134847..fdeec346 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -146,7 +146,6 @@ class MessageManager: """管理所有聊天流的消息容器 (不再是单例)""" def __init__(self): - self._processor_task = None self.containers: dict[str, MessageContainer] = {} self.storage = MessageStorage() # 添加 storage 实例 self._running = True # 处理器运行状态 From 2ac885c4d73d7ca48e32162ba841b5b84d79130e Mon Sep 17 00:00:00 2001 From: Bakadax Date: Thu, 1 May 2025 13:20:41 +0800 Subject: [PATCH 14/67] =?UTF-8?q?=E4=BF=AE=E5=A4=8DTypeError:=20find=5Fmes?= =?UTF-8?q?sages()=20got=20an=20unexpected=20keyword=20argument=20'filter'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/utils/chat_message_builder.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/plugins/utils/chat_message_builder.py b/src/plugins/utils/chat_message_builder.py index 75fca69c..f30403e3 100644 --- a/src/plugins/utils/chat_message_builder.py +++ b/src/plugins/utils/chat_message_builder.py @@ -30,7 +30,7 @@ def get_raw_msg_by_timestamp( filter_query = {"time": {"$gt": timestamp_start, "$lt": timestamp_end}} # 只有当 limit 为 0 时才应用外部 sort sort_order = [("time", 1)] if limit == 0 else None - return find_messages(filter=filter_query, sort=sort_order, limit=limit, limit_mode=limit_mode) + return find_messages(message_filter=filter_query, sort=sort_order, limit=limit, limit_mode=limit_mode) def get_raw_msg_by_timestamp_with_chat( @@ -44,7 +44,7 @@ def get_raw_msg_by_timestamp_with_chat( # 只有当 limit 为 0 时才应用外部 sort sort_order = [("time", 1)] if limit == 0 else None # 直接将 limit_mode 传递给 find_messages - return find_messages(filter=filter_query, sort=sort_order, limit=limit, limit_mode=limit_mode) + return find_messages(message_filter=filter_query, sort=sort_order, limit=limit, limit_mode=limit_mode) def get_raw_msg_by_timestamp_with_chat_users( @@ -66,7 +66,7 @@ def get_raw_msg_by_timestamp_with_chat_users( } # 只有当 limit 为 0 时才应用外部 sort sort_order = [("time", 1)] if limit == 0 else None - return find_messages(filter=filter_query, sort=sort_order, limit=limit, limit_mode=limit_mode) + return find_messages(message_filter=filter_query, sort=sort_order, limit=limit, limit_mode=limit_mode) def get_raw_msg_by_timestamp_with_users( @@ -79,7 +79,7 @@ def get_raw_msg_by_timestamp_with_users( filter_query = {"time": {"$gt": timestamp_start, "$lt": timestamp_end}, "user_id": {"$in": person_ids}} # 只有当 limit 为 0 时才应用外部 sort sort_order = [("time", 1)] if limit == 0 else None - return find_messages(filter=filter_query, sort=sort_order, limit=limit, limit_mode=limit_mode) + return find_messages(message_filter=filter_query, sort=sort_order, limit=limit, limit_mode=limit_mode) def get_raw_msg_before_timestamp(timestamp: float, limit: int = 0) -> List[Dict[str, Any]]: @@ -88,7 +88,7 @@ def get_raw_msg_before_timestamp(timestamp: float, limit: int = 0) -> List[Dict[ """ filter_query = {"time": {"$lt": timestamp}} sort_order = [("time", 1)] - return find_messages(filter=filter_query, sort=sort_order, limit=limit) + return find_messages(message_filter=filter_query, sort=sort_order, limit=limit) def get_raw_msg_before_timestamp_with_chat(chat_id: str, timestamp: float, limit: int = 0) -> List[Dict[str, Any]]: @@ -97,7 +97,7 @@ def get_raw_msg_before_timestamp_with_chat(chat_id: str, timestamp: float, limit """ filter_query = {"chat_id": chat_id, "time": {"$lt": timestamp}} sort_order = [("time", 1)] - return find_messages(filter=filter_query, sort=sort_order, limit=limit) + return find_messages(message_filter=filter_query, sort=sort_order, limit=limit) def get_raw_msg_before_timestamp_with_users(timestamp: float, person_ids: list, limit: int = 0) -> List[Dict[str, Any]]: @@ -106,7 +106,7 @@ def get_raw_msg_before_timestamp_with_users(timestamp: float, person_ids: list, """ filter_query = {"time": {"$lt": timestamp}, "user_id": {"$in": person_ids}} sort_order = [("time", 1)] - return find_messages(filter=filter_query, sort=sort_order, limit=limit) + return find_messages(message_filter=filter_query, sort=sort_order, limit=limit) def num_new_messages_since(chat_id: str, timestamp_start: float = 0.0, timestamp_end: float = None) -> int: From de68a6a8ee52e1757572177a6b5375dd0fb276ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E7=8C=AB?= Date: Thu, 1 May 2025 14:34:59 +0900 Subject: [PATCH 15/67] =?UTF-8?q?fix:=20=E6=9B=B4=E6=94=B9=20MessageManage?= =?UTF-8?q?r=20=E7=9A=84=E5=B1=9E=E6=80=A7=E6=A3=80=E6=9F=A5=E6=96=B9?= =?UTF-8?q?=E5=BC=8F=E4=BB=A5=E9=98=B2=E6=AD=A2=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/plugins/chat/message_sender.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index fdeec346..0f860551 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -146,6 +146,7 @@ class MessageManager: """管理所有聊天流的消息容器 (不再是单例)""" def __init__(self): + self._processor_task = None self.containers: dict[str, MessageContainer] = {} self.storage = MessageStorage() # 添加 storage 实例 self._running = True # 处理器运行状态 @@ -155,7 +156,7 @@ class MessageManager: async def start(self): """启动后台处理器任务。""" # 检查是否已有任务在运行,避免重复启动 - if hasattr(self, "_processor_task") and not self._processor_task.done(): + if self._processor_task is not None and not self._processor_task.done(): logger.warning("Processor task already running.") return self._processor_task = asyncio.create_task(self._start_processor_loop()) @@ -164,7 +165,7 @@ class MessageManager: def stop(self): """停止后台处理器任务。""" self._running = False - if hasattr(self, "_processor_task") and not self._processor_task.done(): + if self._processor_task is not None and not self._processor_task.done(): self._processor_task.cancel() logger.debug("MessageManager processor task stopping.") else: From 95a025b69949e4b3d9c9dca12266e97980f7936e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E7=8C=AB?= Date: Thu, 1 May 2025 14:36:17 +0900 Subject: [PATCH 16/67] add typing --- src/plugins/chat/message_sender.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index 0f860551..ed8c5405 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -1,6 +1,7 @@ # src/plugins/chat/message_sender.py import asyncio import time +from asyncio import Task from typing import Union # from ...common.database import db # 数据库依赖似乎不需要了,注释掉 @@ -146,7 +147,7 @@ class MessageManager: """管理所有聊天流的消息容器 (不再是单例)""" def __init__(self): - self._processor_task = None + self._processor_task: Task | None = None self.containers: dict[str, MessageContainer] = {} self.storage = MessageStorage() # 添加 storage 实例 self._running = True # 处理器运行状态 From b6ce5746de7fec3605c2ff70ea3d94e10dd7b5ca Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 1 May 2025 16:55:51 +0800 Subject: [PATCH 17/67] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E5=A4=8D=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E5=8F=91=E5=B8=83=E5=87=BA=E5=8E=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/heartFC_chat/heartFC_chat.py | 4 ++-- src/plugins/heartFC_chat/heartFC_sender.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 71b8d81c..c2077fad 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -1371,9 +1371,9 @@ class HeartFChatting: if not mark_head: mark_head = True first_bot_msg = bot_message # 保存第一个成功发送的消息对象 - await self.heart_fc_sender.type_and_send_message(bot_message, type=False) + await self.heart_fc_sender.type_and_send_message(bot_message, typing=False) else: - await self.heart_fc_sender.type_and_send_message(bot_message, type=True) + await self.heart_fc_sender.type_and_send_message(bot_message, typing=True) reply_message_ids.append(part_message_id) # 记录我们生成的ID diff --git a/src/plugins/heartFC_chat/heartFC_sender.py b/src/plugins/heartFC_chat/heartFC_sender.py index fc96207d..3e0daea6 100644 --- a/src/plugins/heartFC_chat/heartFC_sender.py +++ b/src/plugins/heartFC_chat/heartFC_sender.py @@ -4,6 +4,7 @@ from typing import Dict, Optional # 重新导入类型 from ..chat.message import MessageSending, MessageThinking # 只保留 MessageSending 和 MessageThinking from ..storage.storage import MessageStorage from ..chat.utils import truncate_message +from src.plugins.message.api import global_api from src.common.logger_manager import get_logger from src.plugins.chat.utils import calculate_typing_time @@ -17,7 +18,7 @@ async def send_message(message: MessageSending) -> None: try: # 直接调用API发送消息 - await send_message(message) + await global_api.send_message(message) logger.success(f"发送消息 '{message_preview}' 成功") except Exception as e: From 6ff552d9210202538bc244e4af5071ed68555a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E7=8C=AB?= Date: Thu, 1 May 2025 17:56:33 +0900 Subject: [PATCH 18/67] fix: update message sending to use global API --- src/plugins/heartFC_chat/heartFC_sender.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/heartFC_chat/heartFC_sender.py b/src/plugins/heartFC_chat/heartFC_sender.py index fc96207d..2681dcfa 100644 --- a/src/plugins/heartFC_chat/heartFC_sender.py +++ b/src/plugins/heartFC_chat/heartFC_sender.py @@ -2,6 +2,7 @@ import asyncio # 重新导入 asyncio from typing import Dict, Optional # 重新导入类型 from ..chat.message import MessageSending, MessageThinking # 只保留 MessageSending 和 MessageThinking +from ..message import global_api from ..storage.storage import MessageStorage from ..chat.utils import truncate_message from src.common.logger_manager import get_logger @@ -17,7 +18,7 @@ async def send_message(message: MessageSending) -> None: try: # 直接调用API发送消息 - await send_message(message) + await global_api.send_message(message) logger.success(f"发送消息 '{message_preview}' 成功") except Exception as e: From 010fec7a535fd5297ba2c7d8ac17a50808493b78 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 1 May 2025 18:02:13 +0800 Subject: [PATCH 19/67] =?UTF-8?q?=E9=87=8D=E6=9E=84=EF=BC=9A=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E4=B8=8D=E5=86=8D=E4=BD=BF=E7=94=A8=E7=9A=84reasoning?= =?UTF-8?q?=5Fprompt=5Fbuilder=EF=BC=8C=E4=BC=98=E5=8C=96=E5=BF=83?= =?UTF-8?q?=E6=B5=81=E6=8E=A7=E5=88=B6=E9=80=BB=E8=BE=91=EF=BC=8C=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E8=81=8A=E5=A4=A9=E5=93=8D=E5=BA=94=E7=94=9F=E6=88=90?= =?UTF-8?q?=E6=96=B9=E5=BC=8F=EF=BC=8C=E7=AE=80=E5=8C=96=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/heartFC_chat/heartFC_sender.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/heartFC_chat/heartFC_sender.py b/src/plugins/heartFC_chat/heartFC_sender.py index 33585f1a..6be3ecd0 100644 --- a/src/plugins/heartFC_chat/heartFC_sender.py +++ b/src/plugins/heartFC_chat/heartFC_sender.py @@ -5,7 +5,6 @@ from ..chat.message import MessageSending, MessageThinking # 只保留 MessageS from ..message import global_api from ..storage.storage import MessageStorage from ..chat.utils import truncate_message -from src.plugins.message.api import global_api from src.common.logger_manager import get_logger from src.plugins.chat.utils import calculate_typing_time @@ -145,3 +144,4 @@ class HeartFCSender: logger.error(f"[{chat_id}] 处理或存储消息 {message_id} 时出错: {e}") # 重新抛出异常,让调用者知道失败了 raise e + From 27d42cbecc0ed93117c8cb02e91b6f38d2149b1a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 1 May 2025 10:03:22 +0000 Subject: [PATCH 20/67] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/heartFC_chat/heartFC_sender.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plugins/heartFC_chat/heartFC_sender.py b/src/plugins/heartFC_chat/heartFC_sender.py index 6be3ecd0..2681dcfa 100644 --- a/src/plugins/heartFC_chat/heartFC_sender.py +++ b/src/plugins/heartFC_chat/heartFC_sender.py @@ -144,4 +144,3 @@ class HeartFCSender: logger.error(f"[{chat_id}] 处理或存储消息 {message_id} 时出错: {e}") # 重新抛出异常,让调用者知道失败了 raise e - From 299cf390ed55b1c339758b972fc1584d205e3249 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 1 May 2025 18:11:15 +0800 Subject: [PATCH 21/67] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=9B=9E=E5=A4=8D?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E6=92=92=E9=80=81=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/heartFC_chat/heartFC_sender.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/heartFC_chat/heartFC_sender.py b/src/plugins/heartFC_chat/heartFC_sender.py index 2681dcfa..47e92e07 100644 --- a/src/plugins/heartFC_chat/heartFC_sender.py +++ b/src/plugins/heartFC_chat/heartFC_sender.py @@ -2,7 +2,8 @@ import asyncio # 重新导入 asyncio from typing import Dict, Optional # 重新导入类型 from ..chat.message import MessageSending, MessageThinking # 只保留 MessageSending 和 MessageThinking -from ..message import global_api +# from ..message import global_api +from src.plugins.message.api import global_api from ..storage.storage import MessageStorage from ..chat.utils import truncate_message from src.common.logger_manager import get_logger From b738008c84b1bae11fe2316af7648a6e93f2b529 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 1 May 2025 10:11:51 +0000 Subject: [PATCH 22/67] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/heartFC_chat/heartFC_sender.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/heartFC_chat/heartFC_sender.py b/src/plugins/heartFC_chat/heartFC_sender.py index 47e92e07..ee4e86ee 100644 --- a/src/plugins/heartFC_chat/heartFC_sender.py +++ b/src/plugins/heartFC_chat/heartFC_sender.py @@ -2,6 +2,7 @@ import asyncio # 重新导入 asyncio from typing import Dict, Optional # 重新导入类型 from ..chat.message import MessageSending, MessageThinking # 只保留 MessageSending 和 MessageThinking + # from ..message import global_api from src.plugins.message.api import global_api from ..storage.storage import MessageStorage From 31291935b2d967d0c0722b796ebb211ebaa94ae4 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 1 May 2025 18:26:30 +0800 Subject: [PATCH 23/67] =?UTF-8?q?fix=EF=BC=9A=E6=B6=88=E6=81=AF=E5=8F=91?= =?UTF-8?q?=E4=B8=8D=E5=87=BA=E5=8E=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/message_sender.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index ed8c5405..c159a4bb 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -3,6 +3,7 @@ import asyncio import time from asyncio import Task from typing import Union +from src.plugins.message.api import global_api # from ...common.database import db # 数据库依赖似乎不需要了,注释掉 from .message import MessageSending, MessageThinking, MessageSet @@ -20,7 +21,7 @@ logger = get_logger("sender") async def send_via_ws(message: MessageSending) -> None: """通过 WebSocket 发送消息""" try: - await send_message(message) + await global_api.send_message(message) except Exception as e: logger.error(f"WS发送失败: {e}") raise ValueError(f"未找到平台:{message.message_info.platform} 的url配置,请检查配置文件") from e From dcdf28c7c158f9b578a628b8ad4f2f97d2fc5eec Mon Sep 17 00:00:00 2001 From: Bakadax Date: Thu, 1 May 2025 20:21:49 +0800 Subject: [PATCH 24/67] =?UTF-8?q?PFC=20=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/chat_observer.py | 1 - src/plugins/PFC/observation_info.py | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/plugins/PFC/chat_observer.py b/src/plugins/PFC/chat_observer.py index e66824a2..1a17db90 100644 --- a/src/plugins/PFC/chat_observer.py +++ b/src/plugins/PFC/chat_observer.py @@ -39,7 +39,6 @@ class ChatObserver: stream_id: 聊天流ID """ self.last_check_time = None - self.last_check_time = None self.last_bot_speak_time = None self.last_user_speak_time = None if stream_id in self._instances: diff --git a/src/plugins/PFC/observation_info.py b/src/plugins/PFC/observation_info.py index af7f537b..fb4f27e9 100644 --- a/src/plugins/PFC/observation_info.py +++ b/src/plugins/PFC/observation_info.py @@ -157,10 +157,10 @@ class ObservationInfo: # #spec (暂时注释掉,如果不需要) # meta_plan_trigger: bool = False - # --- 修改:移除 __post_init__ 的参数 --- - def __init__(self): - self.chat_observer = None - self.chat_observer = None + # 加了会炸,给它注释掉 + # def __init__(self): + # self.chat_observer = None + # self.chat_observer = None def __post_init__(self): """初始化后创建handler并进行必要的设置""" From b9db604d931ec1c0296b3964d7f7ab73043dc0e1 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Thu, 1 May 2025 20:53:06 +0800 Subject: [PATCH 25/67] =?UTF-8?q?=E6=89=8B=E5=8A=A8init?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/observation_info.py | 102 +++++++++++++++++----------- 1 file changed, 63 insertions(+), 39 deletions(-) diff --git a/src/plugins/PFC/observation_info.py b/src/plugins/PFC/observation_info.py index fb4f27e9..b684367e 100644 --- a/src/plugins/PFC/observation_info.py +++ b/src/plugins/PFC/observation_info.py @@ -121,51 +121,70 @@ class ObservationInfoHandler(NotificationHandler): logger.error(traceback.format_exc()) # 打印详细堆栈信息 -@dataclass +# @dataclass <-- 这个,不需要了(递黄瓜) class ObservationInfo: - """决策信息类,用于收集和管理来自chat_observer的通知信息""" + """决策信息类,用于收集和管理来自chat_observer的通知信息 (手动实现 __init__)""" - # --- 修改:添加 private_name 字段 --- - private_name: str = field(init=True) # 让 dataclass 的 __init__ 接收 private_name + # 类型提示保留,可用于文档和静态分析 + private_name: str + chat_history: List[Dict[str, Any]] + chat_history_str: str + unprocessed_messages: List[Dict[str, Any]] + active_users: Set[str] + last_bot_speak_time: Optional[float] + last_user_speak_time: Optional[float] + last_message_time: Optional[float] + last_message_id: Optional[str] + last_message_content: str + last_message_sender: Optional[str] + bot_id: Optional[str] + chat_history_count: int + new_messages_count: int + cold_chat_start_time: Optional[float] + cold_chat_duration: float + is_typing: bool + is_cold_chat: bool + changed: bool + chat_observer: Optional[ChatObserver] + handler: Optional[ObservationInfoHandler] - # data_list - chat_history: List[Dict[str, Any]] = field(default_factory=list) # 修改:明确类型为 Dict - chat_history_str: str = "" - unprocessed_messages: List[Dict[str, Any]] = field(default_factory=list) # 修改:明确类型为 Dict - active_users: Set[str] = field(default_factory=set) - # data - last_bot_speak_time: Optional[float] = None - last_user_speak_time: Optional[float] = None - last_message_time: Optional[float] = None - # 添加 last_message_id - last_message_id: Optional[str] = None - last_message_content: str = "" - last_message_sender: Optional[str] = None - bot_id: Optional[str] = None - chat_history_count: int = 0 - new_messages_count: int = 0 - cold_chat_start_time: Optional[float] = None # 用于计算冷场持续时间 - cold_chat_duration: float = 0.0 # 缓存计算结果 + def __init__(self, private_name: str): + """ + 手动初始化 ObservationInfo 的所有实例变量。 + """ - # state - is_typing: bool = False # 可能表示对方正在输入 - # has_unread_messages: bool = False # 这个状态可以通过 new_messages_count > 0 判断 - is_cold_chat: bool = False - changed: bool = False # 用于标记状态是否有变化,以便外部模块决定是否重新规划 + # 接收的参数 + self.private_name: str = private_name - # #spec (暂时注释掉,如果不需要) - # meta_plan_trigger: bool = False + # data_list + self.chat_history: List[Dict[str, Any]] = [] + self.chat_history_str: str = "" + self.unprocessed_messages: List[Dict[str, Any]] = [] + self.active_users: Set[str] = set() - # 加了会炸,给它注释掉 - # def __init__(self): - # self.chat_observer = None - # self.chat_observer = None + # data + self.last_bot_speak_time: Optional[float] = None + self.last_user_speak_time: Optional[float] = None + self.last_message_time: Optional[float] = None + self.last_message_id: Optional[str] = None + self.last_message_content: str = "" + self.last_message_sender: Optional[str] = None + self.bot_id: Optional[str] = None + self.chat_history_count: int = 0 + self.new_messages_count: int = 0 + self.cold_chat_start_time: Optional[float] = None + self.cold_chat_duration: float = 0.0 - def __post_init__(self): - """初始化后创建handler并进行必要的设置""" - self.chat_observer: Optional[ChatObserver] = None # 添加类型提示 - self.handler = ObservationInfoHandler(self, self.private_name) + # state + self.is_typing: bool = False + self.is_cold_chat: bool = False + self.changed: bool = False + + # 关联对象 + self.chat_observer: Optional[ChatObserver] = None + + self.handler: ObservationInfoHandler = ObservationInfoHandler(self, self.private_name) def bind_to_chat_observer(self, chat_observer: ChatObserver): """绑定到指定的chat_observer @@ -179,6 +198,11 @@ class ObservationInfo: self.chat_observer = chat_observer try: + if not self.handler: # 确保 handler 已经被创建 + logger.error(f"[私聊][{self.private_name}] 尝试绑定时 handler 未初始化!") + self.chat_observer = None # 重置,防止后续错误 + return + # 注册关心的通知类型 self.chat_observer.notification_manager.register_handler( target="observation_info", notification_type=NotificationType.NEW_MESSAGE, handler=self.handler @@ -197,7 +221,7 @@ class ObservationInfo: def unbind_from_chat_observer(self): """解除与chat_observer的绑定""" - if self.chat_observer and hasattr(self.chat_observer, "notification_manager"): # 增加检查 + if self.chat_observer and hasattr(self.chat_observer, "notification_manager") and self.handler: # 增加 handler 检查 try: self.chat_observer.notification_manager.unregister_handler( target="observation_info", notification_type=NotificationType.NEW_MESSAGE, handler=self.handler @@ -215,7 +239,7 @@ class ObservationInfo: finally: # 确保 chat_observer 被重置 self.chat_observer = None else: - logger.warning(f"[私聊][{self.private_name}]尝试解绑时 ChatObserver 不存在或无效") + logger.warning(f"[私聊][{self.private_name}]尝试解绑时 ChatObserver 不存在、无效或 handler 未设置") # 修改:update_from_message 接收 UserInfo 对象 async def update_from_message(self, message: Dict[str, Any], user_info: Optional[UserInfo]): From 9888d879bb1837596e1363c941b3908752fe2efe Mon Sep 17 00:00:00 2001 From: Bakadax Date: Thu, 1 May 2025 20:57:15 +0800 Subject: [PATCH 26/67] ruff --- src/plugins/PFC/observation_info.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plugins/PFC/observation_info.py b/src/plugins/PFC/observation_info.py index b684367e..ae62c54c 100644 --- a/src/plugins/PFC/observation_info.py +++ b/src/plugins/PFC/observation_info.py @@ -1,7 +1,6 @@ from typing import List, Optional, Dict, Any, Set from maim_message import UserInfo import time -from dataclasses import dataclass, field from src.common.logger import get_module_logger from .chat_observer import ChatObserver from .chat_states import NotificationHandler, NotificationType, Notification From aace21983e2d4883f288934f0cb101e8ac38df36 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 1 May 2025 12:57:37 +0000 Subject: [PATCH 27/67] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/observation_info.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/plugins/PFC/observation_info.py b/src/plugins/PFC/observation_info.py index ae62c54c..c7572955 100644 --- a/src/plugins/PFC/observation_info.py +++ b/src/plugins/PFC/observation_info.py @@ -147,7 +147,6 @@ class ObservationInfo: chat_observer: Optional[ChatObserver] handler: Optional[ObservationInfoHandler] - def __init__(self, private_name: str): """ 手动初始化 ObservationInfo 的所有实例变量。 @@ -197,9 +196,9 @@ class ObservationInfo: self.chat_observer = chat_observer try: - if not self.handler: # 确保 handler 已经被创建 + if not self.handler: # 确保 handler 已经被创建 logger.error(f"[私聊][{self.private_name}] 尝试绑定时 handler 未初始化!") - self.chat_observer = None # 重置,防止后续错误 + self.chat_observer = None # 重置,防止后续错误 return # 注册关心的通知类型 @@ -220,7 +219,9 @@ class ObservationInfo: def unbind_from_chat_observer(self): """解除与chat_observer的绑定""" - if self.chat_observer and hasattr(self.chat_observer, "notification_manager") and self.handler: # 增加 handler 检查 + if ( + self.chat_observer and hasattr(self.chat_observer, "notification_manager") and self.handler + ): # 增加 handler 检查 try: self.chat_observer.notification_manager.unregister_handler( target="observation_info", notification_type=NotificationType.NEW_MESSAGE, handler=self.handler From 462fac2547dcd6f59056337396af2321908d5f59 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 1 May 2025 21:38:38 +0800 Subject: [PATCH 28/67] =?UTF-8?q?feat=EF=BC=9A=E4=B8=BAHFC=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E7=A7=81=E8=81=8A=E7=89=B9=E6=AE=8Aprompt=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/config.py | 2 +- src/heart_flow/background_tasks.py | 25 ++- src/heart_flow/observation.py | 92 ++++++++-- src/heart_flow/sub_heartflow.py | 45 +++-- src/heart_flow/subheartflow_manager.py | 173 ++++++++++++++---- src/heart_flow/utils_chat.py | 76 ++++++++ src/plugins/heartFC_chat/heartFC_chat.py | 42 +++-- .../heartFC_chat/heartflow_prompt_builder.py | 16 +- src/plugins/heartFC_chat/normal_chat.py | 67 ++++--- 9 files changed, 417 insertions(+), 121 deletions(-) create mode 100644 src/heart_flow/utils_chat.py diff --git a/src/config/config.py b/src/config/config.py index 033d57f5..9b4386e7 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -22,7 +22,7 @@ logger = get_logger("config") # 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码 is_test = False mai_version_main = "0.6.3" -mai_version_fix = "fix-1" +mai_version_fix = "fix-2" if mai_version_fix: if is_test: diff --git a/src/heart_flow/background_tasks.py b/src/heart_flow/background_tasks.py index 1b64c205..99816540 100644 --- a/src/heart_flow/background_tasks.py +++ b/src/heart_flow/background_tasks.py @@ -19,6 +19,8 @@ INTEREST_EVAL_INTERVAL_SECONDS = 5 NORMAL_CHAT_TIMEOUT_CHECK_INTERVAL_SECONDS = 60 # 新增状态评估间隔 HF_JUDGE_STATE_UPDATE_INTERVAL_SECONDS = 60 +# 新增私聊激活检查间隔 +PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS = 5 # 与兴趣评估类似,设为5秒 CLEANUP_INTERVAL_SECONDS = 1200 STATE_UPDATE_INTERVAL_SECONDS = 60 @@ -71,9 +73,10 @@ class BackgroundTaskManager: self._state_update_task: Optional[asyncio.Task] = None self._cleanup_task: Optional[asyncio.Task] = None self._logging_task: Optional[asyncio.Task] = None - self._normal_chat_timeout_check_task: Optional[asyncio.Task] = None # Nyaa~ 添加聊天超时检查任务的引用 - self._hf_judge_state_update_task: Optional[asyncio.Task] = None # Nyaa~ 添加状态评估任务的引用 - self._into_focus_task: Optional[asyncio.Task] = None # Nyaa~ 添加兴趣评估任务的引用 + self._normal_chat_timeout_check_task: Optional[asyncio.Task] = None + self._hf_judge_state_update_task: Optional[asyncio.Task] = None + self._into_focus_task: Optional[asyncio.Task] = None + self._private_chat_activation_task: Optional[asyncio.Task] = None # 新增私聊激活任务引用 self._tasks: List[Optional[asyncio.Task]] = [] # Keep track of all tasks async def start_tasks(self): @@ -124,6 +127,14 @@ class BackgroundTaskManager: f"专注评估任务已启动 间隔:{INTEREST_EVAL_INTERVAL_SECONDS}s", "_into_focus_task", ), + # 新增私聊激活任务配置 + ( + # Use lambda to pass the interval to the runner function + lambda: self._run_private_chat_activation_cycle(PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS), + "debug", + f"私聊激活检查任务已启动 间隔:{PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS}s", + "_private_chat_activation_task", + ), ] # 统一启动所有任务 @@ -277,3 +288,11 @@ class BackgroundTaskManager: interval=INTEREST_EVAL_INTERVAL_SECONDS, task_func=self._perform_into_focus_work, ) + + # 新增私聊激活任务运行器 + async def _run_private_chat_activation_cycle(self, interval: int): + await _run_periodic_loop( + task_name="Private Chat Activation Check", + interval=interval, + task_func=self.subheartflow_manager.sbhf_absent_private_into_focus + ) diff --git a/src/heart_flow/observation.py b/src/heart_flow/observation.py index e34f37d3..06dcdc31 100644 --- a/src/heart_flow/observation.py +++ b/src/heart_flow/observation.py @@ -12,9 +12,31 @@ from src.plugins.utils.chat_message_builder import ( num_new_messages_since, get_person_id_list, ) +from src.plugins.utils.prompt_builder import Prompt, global_prompt_manager +from src.plugins.chat.chat_stream import chat_manager +from typing import Optional +from src.plugins.person_info.person_info import person_info_manager +# Import the new utility function +from .utils_chat import get_chat_type_and_target_info logger = get_logger("observation") +# --- Define Prompt Templates for Chat Summary --- +Prompt( + """这是qq群聊的聊天记录,请总结以下聊天记录的主题: +{chat_logs} +请用一句话概括,包括人物、事件和主要信息,不要分点。""", + "chat_summary_group_prompt" # Template for group chat +) + +Prompt( + """这是你和{chat_target}的私聊记录,请总结以下聊天记录的主题: +{chat_logs} +请用一句话概括,包括事件,时间,和主要信息,不要分点。""", + "chat_summary_private_prompt" # Template for private chat +) +# --- End Prompt Template Definition --- + # 所有观察的基类 class Observation: @@ -34,28 +56,37 @@ class ChattingObservation(Observation): super().__init__("chat", chat_id) self.chat_id = chat_id + # --- Initialize attributes (defaults) --- + self.is_group_chat: bool = False + self.chat_target_info: Optional[dict] = None + # --- End Initialization --- + + # --- Other attributes initialized in __init__ --- self.talking_message = [] self.talking_message_str = "" self.talking_message_str_truncate = "" - self.name = global_config.BOT_NICKNAME self.nick_name = global_config.BOT_ALIAS_NAMES - self.max_now_obs_len = global_config.observation_context_size self.overlap_len = global_config.compressed_length self.mid_memorys = [] self.max_mid_memory_len = global_config.compress_length_limit self.mid_memory_info = "" - self.person_list = [] - self.llm_summary = LLMRequest( model=global_config.llm_observation, temperature=0.7, max_tokens=300, request_type="chat_observation" ) + async def initialize(self): + # --- Use utility function to determine chat type and fetch info --- + self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.chat_id) + logger.debug(f"ChattingObservation {self.chat_id} initialized: is_group={self.is_group_chat}, target_info={self.chat_target_info}") + # --- End using utility function --- + + # Fetch initial messages (existing logic) initial_messages = get_raw_msg_before_timestamp_with_chat(self.chat_id, self.last_observe_time, 10) - self.talking_message = initial_messages # 将这些消息设为初始上下文 + self.talking_message = initial_messages self.talking_message_str = await build_readable_messages(self.talking_message) # 进行一次观察 返回观察结果observe_info @@ -109,18 +140,49 @@ class ChattingObservation(Observation): messages=oldest_messages, timestamp_mode="normal", read_mark=0 ) - # 调用 LLM 总结主题 - prompt = ( - f"请总结以下聊天记录的主题:\n{oldest_messages_str}\n用一句话概括包括人物事件和主要信息,不要分点:" - ) - summary = "没有主题的闲聊" # 默认值 + # --- Build prompt using template --- + prompt = None # Initialize prompt as None try: - summary_result, _ = await self.llm_summary.generate_response_async(prompt) - if summary_result: # 确保结果不为空 - summary = summary_result + # 构建 Prompt - 根据 is_group_chat 选择模板 + if self.is_group_chat: + prompt_template_name = "chat_summary_group_prompt" + prompt = await global_prompt_manager.format_prompt( + prompt_template_name, + chat_logs=oldest_messages_str + ) + else: + # For private chat, add chat_target to the prompt variables + prompt_template_name = "chat_summary_private_prompt" + # Determine the target name for the prompt + chat_target_name = "对方" # Default fallback + if self.chat_target_info: + # Prioritize person_name, then nickname + chat_target_name = self.chat_target_info.get('person_name') or self.chat_target_info.get('user_nickname') or chat_target_name + + # Format the private chat prompt + prompt = await global_prompt_manager.format_prompt( + prompt_template_name, + # Assuming the private prompt template uses {chat_target} + chat_target=chat_target_name, + chat_logs=oldest_messages_str + ) except Exception as e: - logger.error(f"总结主题失败 for chat {self.chat_id}: {e}") - # 保留默认总结 "没有主题的闲聊" + logger.error(f"构建总结 Prompt 失败 for chat {self.chat_id}: {e}") + # prompt remains None + + summary = "没有主题的闲聊" # 默认值 + + if prompt: # Check if prompt was built successfully + try: + summary_result, _, _ = await self.llm_summary.generate_response(prompt) + if summary_result: # 确保结果不为空 + summary = summary_result + except Exception as e: + logger.error(f"总结主题失败 for chat {self.chat_id}: {e}") + # 保留默认总结 "没有主题的闲聊" + else: + logger.warning(f"因 Prompt 构建失败,跳过 LLM 总结 for chat {self.chat_id}") + mid_memory = { "id": str(int(datetime.now().timestamp())), diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index 8d07e6b5..8e00c263 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -13,6 +13,8 @@ from src.plugins.heartFC_chat.normal_chat import NormalChat 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 +from src.plugins.person_info.person_info import person_info_manager +from .utils_chat import get_chat_type_and_target_info # 定义常量 (从 interest.py 移动过来) @@ -238,6 +240,11 @@ class SubHeartflow: self.chat_state_last_time: float = 0 self.history_chat_state: List[Tuple[ChatState, float]] = [] + # --- Initialize attributes --- + self.is_group_chat: bool = False + self.chat_target_info: Optional[dict] = None + # --- End Initialization --- + # 兴趣检测器 self.interest_chatting: InterestChatting = InterestChatting() @@ -260,11 +267,20 @@ class SubHeartflow: 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 + # 日志前缀 - Moved determination to initialize + self.log_prefix = str(subheartflow_id) # Initial default prefix async def initialize(self): - """异步初始化方法,创建兴趣流""" + """异步初始化方法,创建兴趣流并确定聊天类型""" + + # --- Use utility function to determine chat type and fetch info --- + self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.chat_id) + # Update log prefix after getting info (potential stream name) + self.log_prefix = chat_manager.get_stream_name(self.subheartflow_id) or self.subheartflow_id # Keep this line or adjust if utils provides name + logger.debug(f"SubHeartflow {self.chat_id} initialized: is_group={self.is_group_chat}, target_info={self.chat_target_info}") + # --- End using utility function --- + + # Initialize interest system (existing logic) await self.interest_chatting.initialize() logger.debug(f"{self.log_prefix} InterestChatting 实例已初始化。") @@ -286,26 +302,33 @@ class SubHeartflow: async def _start_normal_chat(self) -> bool: """ - 启动 NormalChat 实例, - 进入 CHAT 状态时使用 - - 确保 HeartFChatting 已停止 + 启动 NormalChat 实例,并进行异步初始化。 + 进入 CHAT 状态时使用。 + 确保 HeartFChatting 已停止。 """ await self._stop_heart_fc_chat() # 确保 专注聊天已停止 log_prefix = self.log_prefix try: - # 获取聊天流并创建 NormalChat 实例 + # 获取聊天流并创建 NormalChat 实例 (同步部分) chat_stream = chat_manager.get_stream(self.chat_id) + if not chat_stream: + logger.error(f"{log_prefix} 无法获取 chat_stream,无法启动 NormalChat。") + return False + self.normal_chat_instance = NormalChat(chat_stream=chat_stream, interest_dict=self.get_interest_dict()) + + # 进行异步初始化 + await self.normal_chat_instance.initialize() + # 启动聊天任务 logger.info(f"{log_prefix} 开始普通聊天,随便水群...") - await self.normal_chat_instance.start_chat() # <--- 修正:调用 start_chat + await self.normal_chat_instance.start_chat() # start_chat now ensures init is called again if needed return True except Exception as e: - logger.error(f"{log_prefix} 启动 NormalChat 时出错: {e}") + logger.error(f"{log_prefix} 启动 NormalChat 或其初始化时出错: {e}") logger.error(traceback.format_exc()) - self.normal_chat_instance = None # 启动失败,清理实例 + self.normal_chat_instance = None # 启动/初始化失败,清理实例 return False async def _stop_heart_fc_chat(self): diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index 30119cca..25e5d3cc 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -335,27 +335,35 @@ class SubHeartflowManager: async def sbhf_absent_into_chat(self): """ - 随机选一个 ABSENT 状态的子心流,评估是否应转换为 CHAT 状态。 + 随机选一个 ABSENT 状态的 *群聊* 子心流,评估是否应转换为 CHAT 状态。 每次调用最多转换一个。 + 私聊会被忽略。 """ current_mai_state = self.mai_state_info.get_current_state() chat_limit = current_mai_state.get_normal_chat_max_num() async with self._lock: - # 1. 筛选出所有 ABSENT 状态的子心流 - absent_subflows = [ - hf for hf in self.subheartflows.values() if hf.chat_state.chat_status == ChatState.ABSENT + # 1. 筛选出所有 ABSENT 状态的 *群聊* 子心流 + absent_group_subflows = [ + hf for hf in self.subheartflows.values() + if hf.chat_state.chat_status == ChatState.ABSENT and hf.is_group_chat ] - if not absent_subflows: - logger.debug("没有摸鱼的子心流可以评估。") # 日志太频繁,注释掉 + if not absent_group_subflows: + # logger.debug("没有摸鱼的群聊子心流可以评估。") # 日志太频繁 return # 没有目标,直接返回 # 2. 随机选一个幸运儿 - sub_hf_to_evaluate = random.choice(absent_subflows) + sub_hf_to_evaluate = random.choice(absent_group_subflows) flow_id = sub_hf_to_evaluate.subheartflow_id stream_name = chat_manager.get_stream_name(flow_id) or flow_id log_prefix = f"[{stream_name}]" + + # --- Private chat check (redundant due to filter above, but safe) --- + # if not sub_hf_to_evaluate.is_group_chat: + # logger.debug(f"{log_prefix} 是私聊,跳过 CHAT 状态评估。") + # return + # --- End check --- # 3. 检查 CHAT 上限 current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT) @@ -658,8 +666,10 @@ class SubHeartflowManager: # --- 新增:处理来自 HeartFChatting 的状态转换请求 --- # async def sbhf_focus_into_absent(self, subflow_id: Any): """ - 接收来自 HeartFChatting 的请求,将特定子心流的状态转换为 ABSENT。 + 接收来自 HeartFChatting 的请求,将特定子心流的状态转换为 ABSENT 或 CHAT。 通常在连续多次 "no_reply" 后被调用。 + 对于私聊,总是转换为 ABSENT。 + 对于群聊,随机决定转换为 ABSENT 或 CHAT (如果 CHAT 未达上限)。 Args: subflow_id: 需要转换状态的子心流 ID。 @@ -667,50 +677,44 @@ class SubHeartflowManager: async with self._lock: subflow = self.subheartflows.get(subflow_id) if not subflow: - logger.warning(f"[状态转换请求] 尝试转换不存在的子心流 {subflow_id} 到 ABSENT") + logger.warning(f"[状态转换请求] 尝试转换不存在的子心流 {subflow_id} 到 ABSENT/CHAT") return stream_name = chat_manager.get_stream_name(subflow_id) or subflow_id current_state = subflow.chat_state.chat_status - # 仅当子心流处于 FOCUSED 状态时才进行转换 - # 因为 HeartFChatting 只在 FOCUSED 状态下运行 if current_state == ChatState.FOCUSED: - target_state = ChatState.ABSENT # 默认目标状态 - log_reason = "默认转换" + target_state = ChatState.ABSENT # Default target + log_reason = "默认转换 (私聊或群聊)" - # 决定是去 ABSENT 还是 CHAT - if random.random() < 0.5: - target_state = ChatState.ABSENT - log_reason = "随机选择 ABSENT" - logger.debug(f"[状态转换请求] {stream_name} ({current_state.value}) 随机决定进入 ABSENT") - else: - # 尝试进入 CHAT,先检查限制 - current_mai_state = self.mai_state_info.get_current_state() - chat_limit = current_mai_state.get_normal_chat_max_num() - # 使用不上锁的版本,因为我们已经在锁内 - current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT) - - if current_chat_count < chat_limit: - target_state = ChatState.CHAT - log_reason = f"随机选择 CHAT (当前 {current_chat_count}/{chat_limit})" - logger.debug( - f"[状态转换请求] {stream_name} ({current_state.value}) 随机决定进入 CHAT,未达上限 ({current_chat_count}/{chat_limit})" - ) - else: + # --- Modify logic based on chat type --- # + if subflow.is_group_chat: + # Group chat: Decide between ABSENT or CHAT + if random.random() < 0.5: # 50% chance to try CHAT + current_mai_state = self.mai_state_info.get_current_state() + chat_limit = current_mai_state.get_normal_chat_max_num() + current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT) + + if current_chat_count < chat_limit: + target_state = ChatState.CHAT + log_reason = f"群聊随机选择 CHAT (当前 {current_chat_count}/{chat_limit})" + else: + target_state = ChatState.ABSENT # Fallback to ABSENT if CHAT limit reached + log_reason = f"群聊随机选择 CHAT 但已达上限 ({current_chat_count}/{chat_limit}),转为 ABSENT" + else: # 50% chance to go directly to ABSENT target_state = ChatState.ABSENT - log_reason = f"随机选择 CHAT 但已达上限 ({current_chat_count}/{chat_limit}),转为 ABSENT" - logger.debug( - f"[状态转换请求] {stream_name} ({current_state.value}) 随机决定进入 CHAT,但已达上限 ({current_chat_count}/{chat_limit}),改为进入 ABSENT" - ) + log_reason = "群聊随机选择 ABSENT" + else: + # Private chat: Always go to ABSENT + target_state = ChatState.ABSENT + log_reason = "私聊退出 FOCUSED,转为 ABSENT" + # --- End modification --- # - # 开始转换 logger.info( f"[状态转换请求] 接收到请求,将 {stream_name} (当前: {current_state.value}) 尝试转换为 {target_state.value} ({log_reason})" ) try: await subflow.change_chat_state(target_state) - # 检查最终状态 final_state = subflow.chat_state.chat_status if final_state == target_state: logger.debug(f"[状态转换请求] {stream_name} 状态已成功转换为 {final_state.value}") @@ -728,5 +732,98 @@ class SubHeartflowManager: logger.warning( f"[状态转换请求] 收到对 {stream_name} 的请求,但其状态为 {current_state.value} (非 FOCUSED),不执行转换" ) + # --- 结束新增 --- # + + # --- 新增:处理私聊从 ABSENT 直接到 FOCUSED 的逻辑 --- # + async def sbhf_absent_private_into_focus(self): + """检查 ABSENT 状态的私聊子心流是否有新活动,若有且未达 FOCUSED 上限,则直接转换为 FOCUSED。""" + log_prefix_task = "[私聊激活检查]" + transitioned_count = 0 + checked_count = 0 + + # --- 获取当前状态和 FOCUSED 上限 --- # + current_mai_state = self.mai_state_info.get_current_state() + focused_limit = current_mai_state.get_focused_chat_max_num() + + # --- 检查是否允许 FOCUS 模式 --- # + if not global_config.allow_focus_mode: + # Log less frequently to avoid spam + # if int(time.time()) % 60 == 0: + # logger.debug(f"{log_prefix_task} 配置不允许进入 FOCUSED 状态") + return + + if focused_limit <= 0: + # logger.debug(f"{log_prefix_task} 当前状态 ({current_mai_state.value}) 不允许 FOCUSED 子心流") + return + + async with self._lock: + # --- 获取当前 FOCUSED 计数 (不上锁版本) --- # + current_focused_count = self.count_subflows_by_state_nolock(ChatState.FOCUSED) + + # --- 筛选出所有 ABSENT 状态的私聊子心流 --- # + eligible_subflows = [ + hf for hf in self.subheartflows.values() + if hf.chat_state.chat_status == ChatState.ABSENT and not hf.is_group_chat + ] + checked_count = len(eligible_subflows) + + if not eligible_subflows: + # logger.debug(f"{log_prefix_task} 没有 ABSENT 状态的私聊子心流可以评估。") + return + + # --- 遍历评估每个符合条件的私聊 --- # + for sub_hf in eligible_subflows: + # --- 再次检查 FOCUSED 上限,因为可能有多个同时激活 --- # + if current_focused_count >= focused_limit: + logger.debug(f"{log_prefix_task} 已达专注上限 ({current_focused_count}/{focused_limit}),停止检查后续私聊。") + break # 已满,无需再检查其他私聊 + + flow_id = sub_hf.subheartflow_id + stream_name = chat_manager.get_stream_name(flow_id) or flow_id + log_prefix = f"[{stream_name}]({log_prefix_task})" + + try: + # --- 检查是否有新活动 --- # + observation = sub_hf._get_primary_observation() # 获取主要观察者 + is_active = False + if observation: + # 检查自上次状态变为 ABSENT 后是否有新消息 + # 使用 chat_state_changed_time 可能更精确 + # 加一点点缓冲时间(例如 1 秒)以防时间戳完全相等 + timestamp_to_check = sub_hf.chat_state_changed_time - 1 + has_new = await observation.has_new_messages_since(timestamp_to_check) + if has_new: + is_active = True + logger.debug(f"{log_prefix} 检测到新消息,标记为活跃。") + # 可选:检查兴趣度是否大于0 (如果需要) + # interest_level = await sub_hf.interest_chatting.get_interest() + # if interest_level > 0: + # is_active = True + # logger.debug(f"{log_prefix} 检测到兴趣度 > 0 ({interest_level:.2f}),标记为活跃。") + else: + logger.warning(f"{log_prefix} 无法获取主要观察者来检查活动状态。") + + # --- 如果活跃且未达上限,则尝试转换 --- # + if is_active: + logger.info(f"{log_prefix} 检测到活跃且未达专注上限 ({current_focused_count}/{focused_limit}),尝试转换为 FOCUSED。") + await sub_hf.change_chat_state(ChatState.FOCUSED) + # 确认转换成功 + if sub_hf.chat_state.chat_status == ChatState.FOCUSED: + transitioned_count += 1 + current_focused_count += 1 # 更新计数器以供本轮后续检查 + logger.info(f"{log_prefix} 成功进入 FOCUSED 状态。") + else: + logger.warning(f"{log_prefix} 尝试进入 FOCUSED 状态失败。当前状态: {sub_hf.chat_state.chat_status.value}") + # else: # 不活跃,无需操作 + # logger.debug(f"{log_prefix} 未检测到新活动,保持 ABSENT。") + + except Exception as e: + logger.error(f"{log_prefix} 检查私聊活动或转换状态时出错: {e}", exc_info=True) + + # --- 循环结束后记录总结日志 --- # + if transitioned_count > 0: + logger.debug(f"{log_prefix_task} 完成,共检查 {checked_count} 个私聊,{transitioned_count} 个转换为 FOCUSED。") # --- 结束新增 --- # + + # --- 结束新增:处理来自 HeartFChatting 的状态转换请求 --- # diff --git a/src/heart_flow/utils_chat.py b/src/heart_flow/utils_chat.py new file mode 100644 index 00000000..5ad664b1 --- /dev/null +++ b/src/heart_flow/utils_chat.py @@ -0,0 +1,76 @@ +import asyncio +from typing import Optional, Tuple, Dict +from src.common.logger_manager import get_logger +from src.plugins.chat.chat_stream import chat_manager +from src.plugins.person_info.person_info import person_info_manager + +logger = get_logger("heartflow_utils") + +async def get_chat_type_and_target_info(chat_id: str) -> Tuple[bool, Optional[Dict]]: + """ + 获取聊天类型(是否群聊)和私聊对象信息。 + + Args: + chat_id: 聊天流ID + + Returns: + Tuple[bool, Optional[Dict]]: + - bool: 是否为群聊 (True 是群聊, False 是私聊或未知) + - Optional[Dict]: 如果是私聊,包含对方信息的字典;否则为 None。 + 字典包含: platform, user_id, user_nickname, person_id, person_name + """ + is_group_chat = False # Default to private/unknown + chat_target_info = None + + try: + chat_stream = await asyncio.to_thread(chat_manager.get_stream, chat_id) # Use to_thread if get_stream is sync + # If get_stream is already async, just use: chat_stream = await chat_manager.get_stream(chat_id) + + if chat_stream: + if chat_stream.group_info: + is_group_chat = True + chat_target_info = None # Explicitly None for group chat + elif chat_stream.user_info: # It's a private chat + is_group_chat = False + user_info = chat_stream.user_info + platform = chat_stream.platform + user_id = user_info.user_id + + # Initialize target_info with basic info + target_info = { + 'platform': platform, + 'user_id': user_id, + 'user_nickname': user_info.user_nickname, + 'person_id': None, + 'person_name': None + } + + # Try to fetch person info (assuming person_info_manager methods are sync) + try: + # Use asyncio.to_thread for potentially blocking sync calls + person_id = await asyncio.to_thread(person_info_manager.get_person_id, platform, user_id) + person_name = None + if person_id: + person_name = await asyncio.to_thread(person_info_manager.get_value, person_id, "person_name") + + # If person_info_manager methods are async, await them directly: + # person_id = await person_info_manager.get_person_id(platform, user_id) + # person_name = None + # if person_id: + # person_name = await person_info_manager.get_value(person_id, "person_name") + + target_info['person_id'] = person_id + target_info['person_name'] = person_name + except Exception as person_e: + logger.warning(f"获取 person_id 或 person_name 时出错 for {platform}:{user_id} in utils: {person_e}") + + chat_target_info = target_info + else: + logger.warning(f"无法获取 chat_stream for {chat_id} in utils") + # Keep defaults: is_group_chat=False, chat_target_info=None + + except Exception as e: + logger.error(f"获取聊天类型和目标信息时出错 for {chat_id}: {e}", exc_info=True) + # Keep defaults on error + + return is_group_chat, chat_target_info \ No newline at end of file diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index c2077fad..f0a47f1d 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -27,6 +27,7 @@ from src.plugins.chat.utils import process_llm_response from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager from src.plugins.moods.moods import MoodManager from src.individuality.individuality import Individuality +from src.heart_flow.utils_chat import get_chat_type_and_target_info WAITING_TIME_THRESHOLD = 300 # 等待新消息时间阈值,单位秒 @@ -194,7 +195,12 @@ class HeartFChatting: self.on_consecutive_no_reply_callback = on_consecutive_no_reply_callback # 日志前缀 - self.log_prefix: str = f"[{chat_manager.get_stream_name(chat_id) or chat_id}]" + self.log_prefix: str = str(chat_id) # Initial default, will be updated + + # --- Initialize attributes (defaults) --- + self.is_group_chat: bool = False + self.chat_target_info: Optional[dict] = None + # --- End Initialization --- # 动作管理器 self.action_manager = ActionManager() @@ -234,22 +240,34 @@ class HeartFChatting: async def _initialize(self) -> bool: """ - 懒初始化以使用提供的标识符解析chat_stream。 - 确保实例已准备好处理触发器。 + 懒初始化,解析chat_stream, 获取聊天类型和目标信息。 """ if self._initialized: return True - - self.chat_stream = chat_manager.get_stream(self.stream_id) - if not self.chat_stream: - logger.error(f"{self.log_prefix} 获取ChatStream失败。") - return False - - # 更新日志前缀(以防流名称发生变化) - self.log_prefix = f"[{chat_manager.get_stream_name(self.stream_id) or self.stream_id}]" + + # --- Use utility function to determine chat type and fetch info --- + # Note: get_chat_type_and_target_info handles getting the chat_stream internally + self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.stream_id) + + # Update log prefix based on potential stream name (if needed, or get it from chat_stream if util doesn't return it) + # Assuming get_chat_type_and_target_info focuses only on type/target + # We still need the chat_stream object itself for other operations + try: + self.chat_stream = await asyncio.to_thread(chat_manager.get_stream, self.stream_id) + if not self.chat_stream: + logger.error(f"[HFC:{self.stream_id}] 获取ChatStream失败 during _initialize, though util func might have succeeded earlier.") + return False # Cannot proceed without chat_stream object + # Update log prefix using the fetched stream object + self.log_prefix = f"[{chat_manager.get_stream_name(self.stream_id) or self.stream_id}]" + except Exception as e: + logger.error(f"[HFC:{self.stream_id}] 获取ChatStream时出错 in _initialize: {e}") + return False + + logger.debug(f"{self.log_prefix} HeartFChatting initialized: is_group={self.is_group_chat}, target_info={self.chat_target_info}") + # --- End using utility function --- self._initialized = True - logger.debug(f"{self.log_prefix}麦麦感觉到了,可以开始认真水群 ") + logger.debug(f"{self.log_prefix} 麦麦感觉到了,可以开始认真水群 ") return True async def start(self): diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index c6cdf251..42d6d174 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -79,7 +79,7 @@ def init_prompt(): - 避免重复或评价自己的发言 - 不要和自己聊天 -【决策任务】 +决策任务 {action_options_text} 你必须从上面列出的可用行动中选择一个,并说明原因。 @@ -90,20 +90,6 @@ JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query": "reasoning": "string", // 做出此决定的详细理由和思考过程,说明你如何应用了回复原则 "emoji_query": "string" // 可选。如果行动是 'emoji_reply',必须提供表情主题(填写表情包的适用场合);如果行动是 'text_reply' 且你想附带表情,也在此提供表情主题,否则留空字符串 ""。遵循回复原则,不要滥用。 }} - -例如: -{{ - "action": "text_reply", - "reasoning": "用户提到了我,且问题比较具体,适合用文本回复。考虑到内容,可以带上一个微笑表情。", - "emoji_query": "微笑" -}} -或 -{{ - "action": "no_reply", - "reasoning": "我已经连续回复了两次,而且这个话题我不太感兴趣,根据回复原则,选择不回复,等待其他人发言。", - "emoji_query": "" -}} - 请输出你的决策 JSON: """, # 使用三引号避免内部引号问题 "planner_prompt", # 保持名称不变,替换内容 diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py index 9ed63c2d..3668e187 100644 --- a/src/plugins/heartFC_chat/normal_chat.py +++ b/src/plugins/heartFC_chat/normal_chat.py @@ -19,6 +19,7 @@ from src.plugins.chat.chat_stream import ChatStream, chat_manager from src.plugins.person_info.relationship_manager import relationship_manager from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager from src.plugins.utils.timer_calculator import Timer +from src.heart_flow.utils_chat import get_chat_type_and_target_info logger = get_logger("chat") @@ -26,28 +27,46 @@ logger = get_logger("chat") class NormalChat: def __init__(self, chat_stream: ChatStream, interest_dict: dict): - """ - 初始化 NormalChat 实例,针对特定的 ChatStream。 - - Args: - chat_stream (ChatStream): 此 NormalChat 实例关联的聊天流对象。 - """ + """初始化 NormalChat 实例。只进行同步操作。""" + # Basic info from chat_stream (sync) self.chat_stream = chat_stream self.stream_id = chat_stream.stream_id - self.stream_name = chat_manager.get_stream_name(self.stream_id) or self.stream_id + # Get initial stream name, might be updated in initialize + self.stream_name = chat_manager.get_stream_name(self.stream_id) or self.stream_id + # Interest dict self.interest_dict = interest_dict + # --- Initialize attributes (defaults) --- + self.is_group_chat: bool = False + self.chat_target_info: Optional[dict] = None + # --- End Initialization --- + + # Other sync initializations self.gpt = NormalChatGenerator() - self.mood_manager = MoodManager.get_instance() # MoodManager 保持单例 - # 存储此实例的兴趣监控任务 + self.mood_manager = MoodManager.get_instance() self.start_time = time.time() - self.last_speak_time = 0 - self._chat_task: Optional[asyncio.Task] = None - logger.info(f"[{self.stream_name}] NormalChat 实例初始化完成。") + self._initialized = False # Track initialization status + + # logger.info(f"[{self.stream_name}] NormalChat 实例 __init__ 完成 (同步部分)。") + # Avoid logging here as stream_name might not be final + + async def initialize(self): + """异步初始化,获取聊天类型和目标信息。""" + if self._initialized: + return + + # --- Use utility function to determine chat type and fetch info --- + self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.stream_id) + # Update stream_name again after potential async call in util func + self.stream_name = chat_manager.get_stream_name(self.stream_id) or self.stream_id + logger.debug(f"[{self.stream_name}] NormalChat initialized: is_group={self.is_group_chat}, target_info={self.chat_target_info}") + # --- End using utility function --- + self._initialized = True + logger.info(f"[{self.stream_name}] NormalChat 实例 initialize 完成 (异步部分)。") # 改为实例方法 async def _create_thinking_message(self, message: MessageRecv) -> str: @@ -416,22 +435,18 @@ class NormalChat: # 改为实例方法, 移除 chat 参数 async def start_chat(self): - """为此 NormalChat 实例关联的 ChatStream 启动聊天任务(如果尚未运行), - 并在后台处理一次初始的高兴趣消息。""" # 文言文注释示例:启聊之始,若有遗珠,当于暗处拂拭,勿碍正途。 + """先进行异步初始化,然后启动聊天任务。""" + if not self._initialized: + await self.initialize() # Ensure initialized before starting tasks + if self._chat_task is None or self._chat_task.done(): - # --- 修改:使用 create_task 启动初始消息处理 --- - logger.info(f"[{self.stream_name}] 开始后台处理初始兴趣消息...") - # 创建一个任务来处理初始消息,不阻塞当前流程 - _initial_process_task = asyncio.create_task(self._process_initial_interest_messages()) - # 可以考虑给这个任务也添加完成回调来记录日志或处理错误 - # initial_process_task.add_done_callback(...) - # --- 修改结束 --- - - # 启动后台轮询任务 (这部分不变) - logger.info(f"[{self.stream_name}] 启动后台兴趣消息轮询任务...") - polling_task = asyncio.create_task(self._reply_interested_message()) # 注意变量名区分 + logger.info(f"[{self.stream_name}] 开始后台处理初始兴趣消息和轮询任务...") + # Process initial messages first + await self._process_initial_interest_messages() + # Then start polling task + polling_task = asyncio.create_task(self._reply_interested_message()) polling_task.add_done_callback(lambda t: self._handle_task_completion(t)) - self._chat_task = polling_task # self._chat_task 仍然指向主要的轮询任务 + self._chat_task = polling_task else: logger.info(f"[{self.stream_name}] 聊天轮询任务已在运行中。") From f4e9721e36e4a8a7f92f4a1514e831e003d59e9d Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 1 May 2025 22:33:59 +0800 Subject: [PATCH 29/67] =?UTF-8?q?feat=EF=BC=9A=E8=BF=9B=E4=B8=80=E6=AD=A5?= =?UTF-8?q?=E5=AE=8C=E5=96=84hfc=E7=A7=81=E8=81=8A=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/heart_flow/sub_mind.py | 138 ++++-- src/heart_flow/utils_chat.py | 13 +- src/plugins/heartFC_chat/heartFC_chat.py | 343 +++++--------- .../heartFC_chat/heartflow_prompt_builder.py | 447 ++++++++++++------ 4 files changed, 534 insertions(+), 407 deletions(-) diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index f414e6b2..3aa1c11d 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -20,34 +20,62 @@ logger = get_logger("sub_heartflow") def init_prompt(): - prompt = "" - prompt += "{extra_info}\n" - prompt += "{relation_prompt}\n" - prompt += "你的名字是{bot_name},{prompt_personality}\n" - prompt += "{last_loop_prompt}\n" - prompt += "{cycle_info_block}\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" + # --- Group Chat Prompt --- + group_prompt = """ +{extra_info} +{relation_prompt} +你的名字是{bot_name},{prompt_personality} +{last_loop_prompt} +{cycle_info_block} +现在是{time_now},你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容: +{chat_observe_info} - Prompt(prompt, "sub_heartflow_prompt_before") +你现在{mood_info} +请仔细阅读当前群聊内容,分析讨论话题和群成员关系,分析你刚刚发言和别人对你的发言的反应,思考你要不要回复。然后思考你是否需要使用函数工具。 +思考并输出你的内心想法 +输出要求: +1. 根据聊天内容生成你的想法,{hf_do_next} +2. 不要分点、不要使用表情符号 +3. 避免多余符号(冒号、引号、括号等) +4. 语言简洁自然,不要浮夸 +5. 如果你刚发言,并且没有人回复你,不要回复 +工具使用说明: +1. 输出想法后考虑是否需要使用工具 +2. 工具可获取信息或执行操作 +3. 如需处理消息或回复,请使用工具。""" + Prompt(group_prompt, "sub_heartflow_prompt_before") - prompt = "" - prompt += "刚刚你的内心想法是:{current_thinking_info}\n" - prompt += "{if_replan_prompt}\n" + # --- Private Chat Prompt --- + private_prompt = """ +{extra_info} +{relation_prompt} +你的名字是{bot_name},{prompt_personality} +{last_loop_prompt} +{cycle_info_block} +现在是{time_now},你正在上网,和 {chat_target_name} 私聊,以下是你们的聊天内容: +{chat_observe_info} - Prompt(prompt, "last_loop") +你现在{mood_info} +请仔细阅读聊天内容,分析你和 {chat_target_name} 的关系,分析你刚刚发言和对方的反应,思考你要不要回复。然后思考你是否需要使用函数工具。 +思考并输出你的内心想法 +输出要求: +1. 根据聊天内容生成你的想法,{hf_do_next} +2. 不要分点、不要使用表情符号 +3. 避免多余符号(冒号、引号、括号等) +4. 语言简洁自然,不要浮夸 +5. 如果你刚发言,对方没有回复你,请谨慎回复 +工具使用说明: +1. 输出想法后考虑是否需要使用工具 +2. 工具可获取信息或执行操作 +3. 如需处理消息或回复,请使用工具。""" + Prompt(private_prompt, "sub_heartflow_prompt_private_before") # New template name + + # --- Last Loop Prompt (remains the same) --- + last_loop_t = """ +刚刚你的内心想法是:{current_thinking_info} +{if_replan_prompt} +""" + Prompt(last_loop_t, "last_loop") def calculate_similarity(text_a: str, text_b: str) -> float: @@ -122,11 +150,18 @@ class SubMind: mood_info = self.chat_state.mood # 获取观察对象 - observation = self.observations[0] - if not observation: - logger.error(f"{self.log_prefix} 无法获取观察对象") - self.update_current_mind("(我没看到任何聊天内容...)") + observation = self.observations[0] if self.observations else None + if not observation or not hasattr(observation, 'is_group_chat'): # Ensure it's ChattingObservation or similar + logger.error(f"{self.log_prefix} 无法获取有效的观察对象或缺少聊天类型信息") + self.update_current_mind("(观察出错了...)") return self.current_mind, self.past_mind + + is_group_chat = observation.is_group_chat + chat_target_info = observation.chat_target_info + chat_target_name = "对方" # Default for private + if not is_group_chat and chat_target_info: + chat_target_name = chat_target_info.get('person_name') or chat_target_info.get('user_nickname') or chat_target_name + # --- End getting observation info --- # 获取观察内容 chat_observe_info = observation.get_observe_info() @@ -238,19 +273,38 @@ class SubMind: )[0] # ---------- 4. 构建最终提示词 ---------- - # 获取提示词模板并填充数据 - prompt = (await global_prompt_manager.get_prompt_async("sub_heartflow_prompt_before")).format( - extra_info="", # 可以在这里添加额外信息 - prompt_personality=prompt_personality, - relation_prompt=relation_prompt, - bot_name=individuality.name, - 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, - cycle_info_block=cycle_info_block, - ) + # --- Choose template based on chat type --- + if is_group_chat: + template_name = "sub_heartflow_prompt_before" + prompt = (await global_prompt_manager.get_prompt_async(template_name)).format( + extra_info="", + prompt_personality=prompt_personality, + relation_prompt=relation_prompt, + bot_name=individuality.name, + 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, + cycle_info_block=cycle_info_block, + # chat_target_name is not used in group prompt + ) + else: # Private chat + template_name = "sub_heartflow_prompt_private_before" + prompt = (await global_prompt_manager.get_prompt_async(template_name)).format( + extra_info="", + prompt_personality=prompt_personality, + relation_prompt=relation_prompt, # Might need adjustment for private context + bot_name=individuality.name, + time_now=time_now, + chat_target_name=chat_target_name, # Pass target name + chat_observe_info=chat_observe_info, + mood_info=mood_info, + hf_do_next=hf_do_next, + last_loop_prompt=last_loop_prompt, + cycle_info_block=cycle_info_block, + ) + # --- End choosing template --- # ---------- 5. 执行LLM请求并处理响应 ---------- content = "" # 初始化内容变量 diff --git a/src/heart_flow/utils_chat.py b/src/heart_flow/utils_chat.py index 5ad664b1..fcbcc738 100644 --- a/src/heart_flow/utils_chat.py +++ b/src/heart_flow/utils_chat.py @@ -45,20 +45,15 @@ async def get_chat_type_and_target_info(chat_id: str) -> Tuple[bool, Optional[Di 'person_name': None } - # Try to fetch person info (assuming person_info_manager methods are sync) + # Try to fetch person info try: - # Use asyncio.to_thread for potentially blocking sync calls + # Assume get_person_id is sync (as per original code), keep using to_thread person_id = await asyncio.to_thread(person_info_manager.get_person_id, platform, user_id) person_name = None if person_id: - person_name = await asyncio.to_thread(person_info_manager.get_value, person_id, "person_name") + # get_value is async, so await it directly + person_name = await person_info_manager.get_value(person_id, "person_name") - # If person_info_manager methods are async, await them directly: - # person_id = await person_info_manager.get_person_id(platform, user_id) - # person_name = None - # if person_id: - # person_name = await person_info_manager.get_value(person_id, "person_name") - target_info['person_id'] = person_id target_info['person_name'] = person_name except Exception as person_e: diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index f0a47f1d..260fb440 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -851,18 +851,15 @@ class HeartFChatting: f"{self.log_prefix}[Planner] 临时移除的动作: {actions_to_remove_temporarily}, 当前可用: {list(current_available_actions.keys())}" ) - # --- 构建提示词 (调用修改后的 _build_planner_prompt) --- - # replan_prompt_str = "" # 暂时简化 - # if is_re_planned: - # replan_prompt_str = await self._build_replan_prompt( - # self._current_cycle.action_type, self._current_cycle.reasoning - # ) - prompt = await self._build_planner_prompt( - observed_messages_str, - current_mind, - self.sub_mind.structured_info, - "", # replan_prompt_str, - current_available_actions, # <--- 传入当前可用动作 + # --- 构建提示词 (调用修改后的 PromptBuilder 方法) --- + prompt = await prompt_builder.build_planner_prompt( + is_group_chat=self.is_group_chat, # <-- Pass HFC state + chat_target_info=self.chat_target_info, # <-- Pass HFC state + cycle_history=self._cycle_history, # <-- Pass HFC state + observed_messages_str=observed_messages_str, # <-- Pass local variable + current_mind=current_mind, # <-- Pass argument + structured_info=self.sub_mind.structured_info, # <-- Pass SubMind info + current_available_actions=current_available_actions # <-- Pass determined actions ) # --- 调用 LLM (普通文本生成) --- @@ -1126,217 +1123,6 @@ class HeartFChatting: return prompt - async def _build_planner_prompt( - self, - observed_messages_str: str, - current_mind: Optional[str], - structured_info: Dict[str, Any], - replan_prompt: str, - current_available_actions: Dict[str, str], - ) -> str: - """构建 Planner LLM 的提示词 (获取模板并填充数据)""" - try: - # 准备结构化信息块 - structured_info_block = "" - if structured_info: - structured_info_block = f"以下是一些额外的信息:\n{structured_info}\n" - - # 准备聊天内容块 - chat_content_block = "" - if observed_messages_str: - chat_content_block = "观察到的最新聊天内容如下:\n---\n" - chat_content_block += observed_messages_str - chat_content_block += "\n---" - else: - chat_content_block = "当前没有观察到新的聊天内容。\n" - - # 准备当前思维块 (修改以匹配模板) - current_mind_block = "" - if current_mind: - # 模板中占位符是 {current_mind_block},它期望包含"你的内心想法:"的前缀 - current_mind_block = f"你的内心想法:\n{current_mind}" - else: - current_mind_block = "你的内心想法:\n[没有特别的想法]" - - # 准备循环信息块 (分析最近的活动循环) - recent_active_cycles = [] - for cycle in reversed(self._cycle_history): - # 只关心实际执行了动作的循环 - if cycle.action_taken: - recent_active_cycles.append(cycle) - # 最多找最近的3个活动循环 - if len(recent_active_cycles) == 3: - break - - cycle_info_block = "" - consecutive_text_replies = 0 - responses_for_prompt = [] - - # 检查这最近的活动循环中有多少是连续的文本回复 (从最近的开始看) - for cycle in recent_active_cycles: - if cycle.action_type == "text_reply": - consecutive_text_replies += 1 - # 获取回复内容,如果不存在则返回'[空回复]' - response_text = cycle.response_info.get("response_text", []) - # 使用简单的 join 来格式化回复内容列表 - formatted_response = "[空回复]" if not response_text else " ".join(response_text) - responses_for_prompt.append(formatted_response) - else: - # 一旦遇到非文本回复,连续性中断 - break - - # 根据连续文本回复的数量构建提示信息 - # 注意: responses_for_prompt 列表是从最近到最远排序的 - if consecutive_text_replies >= 3: # 如果最近的三个活动都是文本回复 - cycle_info_block = f'你已经连续回复了三条消息(最近: "{responses_for_prompt[0]}",第二近: "{responses_for_prompt[1]}",第三近: "{responses_for_prompt[2]}")。你回复的有点多了,请注意' - elif consecutive_text_replies == 2: # 如果最近的两个活动是文本回复 - cycle_info_block = f'你已经连续回复了两条消息(最近: "{responses_for_prompt[0]}",第二近: "{responses_for_prompt[1]}"),请注意' - elif consecutive_text_replies == 1: # 如果最近的一个活动是文本回复 - cycle_info_block = f'你刚刚已经回复一条消息(内容: "{responses_for_prompt[0]}")' - - # 包装提示块,增加可读性,即使没有连续回复也给个标记 - if cycle_info_block: - # 模板中占位符是 {cycle_info_block},它期望包含"【近期回复历史】"的前缀 - cycle_info_block = f"\n【近期回复历史】\n{cycle_info_block}\n" - else: - # 如果最近的活动循环不是文本回复,或者没有活动循环 - cycle_info_block = "\n【近期回复历史】\n(最近没有连续文本回复)\n" - - individuality = Individuality.get_instance() - # 模板中占位符是 {prompt_personality} - prompt_personality = individuality.get_prompt(x_person=2, level=2) - - # --- 构建可用动作描述 (用于填充模板中的 {action_options_text}) --- - action_options_text = "当前你可以选择的行动有:\n" - action_keys = list(current_available_actions.keys()) - for name in action_keys: - desc = current_available_actions[name] - action_options_text += f"- '{name}': {desc}\n" - - # --- 选择一个示例动作键 (用于填充模板中的 {example_action}) --- - example_action_key = action_keys[0] if action_keys else "no_reply" - - # --- 获取提示词模板 --- - planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt") - - # --- 填充模板 --- - prompt = planner_prompt_template.format( - bot_name=global_config.BOT_NICKNAME, - prompt_personality=prompt_personality, - structured_info_block=structured_info_block, - chat_content_block=chat_content_block, - current_mind_block=current_mind_block, - replan="", # 暂时留空 replan 信息 - cycle_info_block=cycle_info_block, - action_options_text=action_options_text, # 传入可用动作描述 - example_action=example_action_key, # 传入示例动作键 - ) - - return prompt - - except Exception as e: - logger.error(f"{self.log_prefix}[Planner] 构建提示词时出错: {e}") - logger.error(traceback.format_exc()) - return "[构建 Planner Prompt 时出错]" # 返回错误提示,避免空字符串 - - # --- 回复器 (Replier) 的定义 --- # - async def _replier_work( - self, - reason: str, - anchor_message: MessageRecv, - thinking_id: str, - ) -> Optional[List[str]]: - """ - 回复器 (Replier): 核心逻辑,负责生成回复文本。 - (已整合原 HeartFCGenerator 的功能) - """ - try: - # 1. 获取情绪影响因子并调整模型温度 - arousal_multiplier = MoodManager.get_instance().get_arousal_multiplier() - current_temp = global_config.llm_normal["temp"] * arousal_multiplier - self.model_normal.temperature = current_temp # 动态调整温度 - - # 2. 获取信息捕捉器 - info_catcher = info_catcher_manager.get_info_catcher(thinking_id) - - # 3. 构建 Prompt - with Timer("构建Prompt", {}): # 内部计时器,可选保留 - prompt = await prompt_builder.build_prompt( - build_mode="focus", - reason=reason, - current_mind_info=self.sub_mind.current_mind, - structured_info=self.sub_mind.structured_info, - message_txt="", # 似乎是固定的空字符串 - sender_name="", # 似乎是固定的空字符串 - chat_stream=anchor_message.chat_stream, - ) - - # 4. 调用 LLM 生成回复 - content = None - reasoning_content = None - model_name = "unknown_model" - try: - with Timer("LLM生成", {}): # 内部计时器,可选保留 - content, reasoning_content, model_name = await self.model_normal.generate_response(prompt) - # logger.info(f"{self.log_prefix}[Replier-{thinking_id}]\\nPrompt:\\n{prompt}\\n生成回复: {content}\\n") - # 捕捉 LLM 输出信息 - info_catcher.catch_after_llm_generated( - prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=model_name - ) - - except Exception as llm_e: - # 精简报错信息 - logger.error(f"{self.log_prefix}[Replier-{thinking_id}] LLM 生成失败: {llm_e}") - return None # LLM 调用失败则无法生成回复 - - # 5. 处理 LLM 响应 - if not content: - logger.warning(f"{self.log_prefix}[Replier-{thinking_id}] LLM 生成了空内容。") - return None - - with Timer("处理响应", {}): # 内部计时器,可选保留 - processed_response = process_llm_response(content) - - if not processed_response: - logger.warning(f"{self.log_prefix}[Replier-{thinking_id}] 处理后的回复为空。") - return None - - return processed_response - - except Exception as e: - # 更通用的错误处理,精简信息 - logger.error(f"{self.log_prefix}[Replier-{thinking_id}] 回复生成意外失败: {e}") - # logger.error(traceback.format_exc()) # 可以取消注释这行以在调试时查看完整堆栈 - return None - - # --- Methods moved from HeartFCController start --- - async def _create_thinking_message(self, anchor_message: Optional[MessageRecv]) -> Optional[str]: - """创建思考消息 (尝试锚定到 anchor_message)""" - if not anchor_message or not anchor_message.chat_stream: - logger.error(f"{self.log_prefix} 无法创建思考消息,缺少有效的锚点消息或聊天流。") - return None - - chat = anchor_message.chat_stream - messageinfo = anchor_message.message_info - bot_user_info = UserInfo( - user_id=global_config.BOT_QQ, - user_nickname=global_config.BOT_NICKNAME, - platform=messageinfo.platform, - ) - - thinking_time_point = round(time.time(), 2) - thinking_id = "mt" + str(thinking_time_point) - thinking_message = MessageThinking( - message_id=thinking_id, - chat_stream=chat, - bot_user_info=bot_user_info, - reply=anchor_message, # 回复的是锚点消息 - thinking_start_time=thinking_time_point, - ) - # Access MessageManager directly - await self.heart_fc_sender.register_thinking(thinking_message) - return thinking_id - async def _send_response_messages( self, anchor_message: Optional[MessageRecv], response_set: List[str], thinking_id: str ) -> Optional[MessageSending]: @@ -1472,3 +1258,114 @@ class HeartFChatting: if self._cycle_history: return self._cycle_history[-1].to_dict() return None + + # --- 回复器 (Replier) 的定义 --- # + async def _replier_work( + self, + reason: str, + anchor_message: MessageRecv, + thinking_id: str, + ) -> Optional[List[str]]: + """ + 回复器 (Replier): 核心逻辑,负责生成回复文本。 + (已整合原 HeartFCGenerator 的功能) + """ + try: + # 1. 获取情绪影响因子并调整模型温度 + arousal_multiplier = MoodManager.get_instance().get_arousal_multiplier() + current_temp = global_config.llm_normal["temp"] * arousal_multiplier + self.model_normal.temperature = current_temp # 动态调整温度 + + # 2. 获取信息捕捉器 + info_catcher = info_catcher_manager.get_info_catcher(thinking_id) + + # --- Determine sender_name for private chat --- + sender_name_for_prompt = "某人" # Default for group or if info unavailable + if not self.is_group_chat and self.chat_target_info: + # Prioritize person_name, then nickname + sender_name_for_prompt = self.chat_target_info.get('person_name') or self.chat_target_info.get('user_nickname') or sender_name_for_prompt + # --- End determining sender_name --- + + # 3. 构建 Prompt + with Timer("构建Prompt", {}): # 内部计时器,可选保留 + prompt = await prompt_builder.build_prompt( + build_mode="focus", + chat_stream=self.chat_stream, # Pass the stream object + # Focus specific args: + reason=reason, + current_mind_info=self.sub_mind.current_mind, + structured_info=self.sub_mind.structured_info, + sender_name=sender_name_for_prompt, # Pass determined name + # Normal specific args (not used in focus mode): + # message_txt="", + ) + + # 4. 调用 LLM 生成回复 + content = None + reasoning_content = None + model_name = "unknown_model" + if not prompt: + logger.error(f"{self.log_prefix}[Replier-{thinking_id}] Prompt 构建失败,无法生成回复。") + return None + + try: + with Timer("LLM生成", {}): # 内部计时器,可选保留 + content, reasoning_content, model_name = await self.model_normal.generate_response(prompt) + # logger.info(f"{self.log_prefix}[Replier-{thinking_id}]\nPrompt:\n{prompt}\n生成回复: {content}\n") + # 捕捉 LLM 输出信息 + info_catcher.catch_after_llm_generated( + prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=model_name + ) + + except Exception as llm_e: + # 精简报错信息 + logger.error(f"{self.log_prefix}[Replier-{thinking_id}] LLM 生成失败: {llm_e}") + return None # LLM 调用失败则无法生成回复 + + # 5. 处理 LLM 响应 + if not content: + logger.warning(f"{self.log_prefix}[Replier-{thinking_id}] LLM 生成了空内容。") + return None + + with Timer("处理响应", {}): # 内部计时器,可选保留 + processed_response = process_llm_response(content) + + if not processed_response: + logger.warning(f"{self.log_prefix}[Replier-{thinking_id}] 处理后的回复为空。") + return None + + return processed_response + + except Exception as e: + # 更通用的错误处理,精简信息 + logger.error(f"{self.log_prefix}[Replier-{thinking_id}] 回复生成意外失败: {e}") + # logger.error(traceback.format_exc()) # 可以取消注释这行以在调试时查看完整堆栈 + return None + + # --- Methods moved from HeartFCController start --- + async def _create_thinking_message(self, anchor_message: Optional[MessageRecv]) -> Optional[str]: + """创建思考消息 (尝试锚定到 anchor_message)""" + if not anchor_message or not anchor_message.chat_stream: + logger.error(f"{self.log_prefix} 无法创建思考消息,缺少有效的锚点消息或聊天流。") + return None + + chat = anchor_message.chat_stream + messageinfo = anchor_message.message_info + bot_user_info = UserInfo( + user_id=global_config.BOT_QQ, + user_nickname=global_config.BOT_NICKNAME, + platform=messageinfo.platform, + ) + + thinking_time_point = round(time.time(), 2) + thinking_id = "mt" + str(thinking_time_point) + thinking_message = MessageThinking( + message_id=thinking_id, + chat_stream=chat, + bot_user_info=bot_user_info, + reply=anchor_message, # 回复的是锚点消息 + thinking_start_time=thinking_time_point, + ) + # Access MessageManager directly (using heart_fc_sender) + await self.heart_fc_sender.register_thinking(thinking_message) + return thinking_id diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 42d6d174..811b54d5 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -7,13 +7,15 @@ from src.plugins.utils.chat_message_builder import build_readable_messages, get_ from src.plugins.person_info.relationship_manager import relationship_manager from src.plugins.chat.utils import get_embedding import time -from typing import Union, Optional +from typing import Union, Optional, Deque, Dict, Any from ...common.database import db from ..chat.utils import get_recent_group_speaker from ..moods.moods import MoodManager from ..memory_system.Hippocampus import HippocampusManager from ..schedule.schedule_generator import bot_schedule from ..knowledge.knowledge_lib import qa_manager +import traceback +from .heartFC_Cycleinfo import CycleInfo logger = get_logger("prompt") @@ -49,7 +51,7 @@ def init_prompt(): # Planner提示词 - 修改为要求 JSON 输出 Prompt( - """你的名字是{bot_name},{prompt_personality},你现在正在一个群聊中。需要基于以下信息决定如何参与对话: + """你的名字是{bot_name},{prompt_personality},{chat_context_description}。需要基于以下信息决定如何参与对话: {structured_info_block} {chat_content_block} {current_mind_block} @@ -59,25 +61,25 @@ def init_prompt(): 【回复原则】 1. 不回复(no_reply)适用: -- 话题无关/无聊/不感兴趣 -- 最后一条消息是你自己发的且无人回应你 -- 讨论你不懂的专业话题 -- 你发送了太多消息,且无人回复 + - 话题无关/无聊/不感兴趣 + - 最后一条消息是你自己发的且无人回应你 + - 讨论你不懂的专业话题 + - 你发送了太多消息,且无人回复 2. 文字回复(text_reply)适用: -- 有实质性内容需要表达 -- 有人提到你,但你还没有回应他 -- 可以追加emoji_query表达情绪(emoji_query填写表情包的适用场合,也就是当前场合) -- 不要追加太多表情 + - 有实质性内容需要表达 + - 有人提到你,但你还没有回应他 + - 可以追加emoji_query表达情绪(emoji_query填写表情包的适用场合,也就是当前场合) + - 不要追加太多表情 3. 纯表情回复(emoji_reply)适用: -- 适合用表情回应的场景 -- 需提供明确的emoji_query + - 适合用表情回应的场景 + - 需提供明确的emoji_query 4. 自我对话处理: -- 如果是自己发的消息想继续,需自然衔接 -- 避免重复或评价自己的发言 -- 不要和自己聊天 + - 如果是自己发的消息想继续,需自然衔接 + - 避免重复或评价自己的发言 + - 不要和自己聊天 决策任务 {action_options_text} @@ -91,8 +93,8 @@ JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query": "emoji_query": "string" // 可选。如果行动是 'emoji_reply',必须提供表情主题(填写表情包的适用场合);如果行动是 'text_reply' 且你想附带表情,也在此提供表情主题,否则留空字符串 ""。遵循回复原则,不要滥用。 }} 请输出你的决策 JSON: -""", # 使用三引号避免内部引号问题 - "planner_prompt", # 保持名称不变,替换内容 +""", + "planner_prompt", ) Prompt( @@ -136,24 +138,67 @@ JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query": Prompt("你现在正在做的事情是:{schedule_info}", "schedule_prompt") Prompt("\n你有以下这些**知识**:\n{prompt_info}\n请你**记住上面的知识**,之后可能会用到。\n", "knowledge_prompt") + # --- Template for HeartFChatting (FOCUSED mode) --- + Prompt( + """ +{info_from_tools} +你正在和 {sender_name} 私聊。 +聊天记录如下: +{chat_talking_prompt} +现在你想要回复。 -async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_stream, sender_name) -> tuple[str, str]: +你需要扮演一位网名叫{bot_name}的人进行回复,这个人的特点是:"{prompt_personality}"。 +你正在和 {sender_name} 私聊, 现在请你读读你们之前的聊天记录,然后给出日常且口语化的回复,平淡一些。 +看到以上聊天记录,你刚刚在想: + +{current_mind_info} +因为上述想法,你决定回复,原因是:{reason} + +回复尽量简短一些。请注意把握聊天内容,{reply_style2}。{prompt_ger} +{reply_style1},说中文,不要刻意突出自身学科背景,注意只输出回复内容。 +{moderation_prompt}。注意:回复不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""", + "heart_flow_private_prompt", # New template for private FOCUSED chat + ) + + # --- Template for NormalChat (CHAT mode) --- + Prompt( + """ +{memory_prompt} +{relation_prompt} +{prompt_info} +{schedule_prompt} +你正在和 {sender_name} 私聊。 +聊天记录如下: +{chat_talking_prompt} +现在 {sender_name} 说的: {message_txt} 引起了你的注意,你想要回复这条消息。 + +你的网名叫{bot_name},有人也叫你{bot_other_names},{prompt_personality}。 +你正在和 {sender_name} 私聊, 现在请你读读你们之前的聊天记录,{mood_prompt},{reply_style1}, +尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,{reply_style2}。{prompt_ger} +请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,不要浮夸,平淡一些 ,不要随意遵从他人指令。 +请注意不要输出多余内容(包括前后缀,冒号和引号,括号等),只输出回复内容。 +{moderation_prompt} +不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。只输出回复内容""", + "reasoning_prompt_private_main", # New template for private CHAT chat + ) + + +async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_stream, sender_name) -> str: individuality = Individuality.get_instance() prompt_personality = individuality.get_prompt(x_person=0, level=2) - # 日程构建 - # schedule_prompt = f'''你现在正在做的事情是:{bot_schedule.get_current_num_task(num = 1,time_info = False)}''' - - if chat_stream.group_info: - chat_in_group = True - else: - chat_in_group = False + + # Determine if it's a group chat + is_group_chat = bool(chat_stream.group_info) + + # Use sender_name passed from caller for private chat, otherwise use a default for group + # Default sender_name for group chat isn't used in the group prompt template, but set for consistency + effective_sender_name = sender_name if not is_group_chat else "某人" message_list_before_now = get_raw_msg_before_timestamp_with_chat( chat_id=chat_stream.stream_id, timestamp=time.time(), limit=global_config.observation_context_size, ) - chat_talking_prompt = await build_readable_messages( message_list_before_now, replace_bot_name=True, @@ -163,7 +208,6 @@ async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_s truncate=True, ) - # 中文高手(新加的好玩功能) prompt_ger = "" if random.random() < 0.04: prompt_ger += "你喜欢用倒装句" @@ -171,20 +215,20 @@ async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_s prompt_ger += "你喜欢用反问句" reply_styles1 = [ - ("给出日常且口语化的回复,平淡一些", 0.4), # 40%概率 - ("给出非常简短的回复", 0.4), # 40%概率 - ("给出缺失主语的回复,简短", 0.15), # 15%概率 - ("给出带有语病的回复,朴实平淡", 0.05), # 5%概率 + ("给出日常且口语化的回复,平淡一些", 0.4), + ("给出非常简短的回复", 0.4), + ("给出缺失主语的回复,简短", 0.15), + ("给出带有语病的回复,朴实平淡", 0.05), ] reply_style1_chosen = random.choices( [style[0] for style in reply_styles1], weights=[style[1] for style in reply_styles1], k=1 )[0] reply_styles2 = [ - ("不要回复的太有条理,可以有个性", 0.6), # 60%概率 - ("不要回复的太有条理,可以复读", 0.15), # 15%概率 - ("回复的认真一些", 0.2), # 20%概率 - ("可以回复单个表情符号", 0.05), # 5%概率 + ("不要回复的太有条理,可以有个性", 0.6), + ("不要回复的太有条理,可以复读", 0.15), + ("回复的认真一些", 0.2), + ("可以回复单个表情符号", 0.05), ] reply_style2_chosen = random.choices( [style[0] for style in reply_styles2], weights=[style[1] for style in reply_styles2], k=1 @@ -197,31 +241,51 @@ async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_s else: structured_info_prompt = "" - logger.debug("开始构建prompt") + logger.debug("开始构建 focus 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, - bot_name=global_config.BOT_NICKNAME, - prompt_personality=prompt_personality, - chat_target_2=await global_prompt_manager.get_prompt_async("chat_target_group2") - if chat_in_group - else await global_prompt_manager.get_prompt_async("chat_target_private2"), - current_mind_info=current_mind_info, - reply_style2=reply_style2_chosen, - reply_style1=reply_style1_chosen, - reason=reason, - prompt_ger=prompt_ger, - moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), - sender_name=sender_name, - ) - - logger.debug(f"focus_chat_prompt: \n{prompt}") + # --- Choose template based on chat type --- + if is_group_chat: + template_name = "heart_flow_prompt" + # Group specific formatting variables (already fetched or default) + chat_target_1 = await global_prompt_manager.get_prompt_async("chat_target_group1") + chat_target_2 = await global_prompt_manager.get_prompt_async("chat_target_group2") + + prompt = await global_prompt_manager.format_prompt( + template_name, + info_from_tools=structured_info_prompt, + chat_target=chat_target_1, # Used in group template + chat_talking_prompt=chat_talking_prompt, + bot_name=global_config.BOT_NICKNAME, + prompt_personality=prompt_personality, + chat_target_2=chat_target_2, # Used in group template + current_mind_info=current_mind_info, + reply_style2=reply_style2_chosen, + reply_style1=reply_style1_chosen, + reason=reason, + prompt_ger=prompt_ger, + moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), + # sender_name is not used in the group template + ) + else: # Private chat + template_name = "heart_flow_private_prompt" + prompt = await global_prompt_manager.format_prompt( + template_name, + info_from_tools=structured_info_prompt, + sender_name=effective_sender_name, # Used in private template + chat_talking_prompt=chat_talking_prompt, + bot_name=global_config.BOT_NICKNAME, + prompt_personality=prompt_personality, + # chat_target and chat_target_2 are not used in private template + current_mind_info=current_mind_info, + reply_style2=reply_style2_chosen, + reply_style1=reply_style1_chosen, + reason=reason, + prompt_ger=prompt_ger, + moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), + ) + # --- End choosing template --- + logger.debug(f"focus_chat_prompt (is_group={is_group_chat}): \n{prompt}") return prompt @@ -233,13 +297,15 @@ class PromptBuilder: async def build_prompt( self, build_mode, - reason, - current_mind_info, - structured_info, - message_txt: str, - sender_name: str = "某人", - chat_stream=None, - ) -> Optional[tuple[str, str]]: + chat_stream, + reason=None, + current_mind_info=None, + structured_info=None, + message_txt=None, + sender_name = "某人", + ) -> Optional[str]: + is_group_chat = bool(chat_stream.group_info) + if build_mode == "normal": return await self._build_prompt_normal(chat_stream, message_txt, sender_name) @@ -253,54 +319,48 @@ class PromptBuilder: ) return None - async def _build_prompt_normal(self, chat_stream, message_txt: str, sender_name: str = "某人") -> tuple[str, str]: + async def _build_prompt_normal(self, chat_stream, message_txt: str, sender_name: str = "某人") -> str: individuality = Individuality.get_instance() prompt_personality = individuality.get_prompt(x_person=2, level=2) + is_group_chat = bool(chat_stream.group_info) - # 关系 - who_chat_in_group = [ - (chat_stream.user_info.platform, chat_stream.user_info.user_id, chat_stream.user_info.user_nickname) - ] - who_chat_in_group += get_recent_group_speaker( - chat_stream.stream_id, - (chat_stream.user_info.platform, chat_stream.user_info.user_id), - limit=global_config.observation_context_size, - ) - + who_chat_in_group = [] + if is_group_chat: + who_chat_in_group = get_recent_group_speaker( + chat_stream.stream_id, + (chat_stream.user_info.platform, chat_stream.user_info.user_id) if chat_stream.user_info else None, + limit=global_config.observation_context_size, + ) + elif chat_stream.user_info: + who_chat_in_group.append((chat_stream.user_info.platform, chat_stream.user_info.user_id, chat_stream.user_info.user_nickname)) + relation_prompt = "" for person in who_chat_in_group: - relation_prompt += await relationship_manager.build_relationship_info(person) - # print(f"relation_prompt: {relation_prompt}") + if len(person) >= 3 and person[0] and person[1]: + relation_prompt += await relationship_manager.build_relationship_info(person) + else: + logger.warning(f"Invalid person tuple encountered for relationship prompt: {person}") - # print(f"relat11111111ion_prompt: {relation_prompt}") - - # 心情 mood_manager = MoodManager.get_instance() mood_prompt = mood_manager.get_prompt() - - # logger.info(f"心情prompt: {mood_prompt}") - reply_styles1 = [ - ("然后给出日常且口语化的回复,平淡一些", 0.4), # 40%概率 - ("给出非常简短的回复", 0.4), # 40%概率 - ("给出缺失主语的回复", 0.15), # 15%概率 - ("给出带有语病的回复", 0.05), # 5%概率 + ("然后给出日常且口语化的回复,平淡一些", 0.4), + ("给出非常简短的回复", 0.4), + ("给出缺失主语的回复", 0.15), + ("给出带有语病的回复", 0.05), ] reply_style1_chosen = random.choices( [style[0] for style in reply_styles1], weights=[style[1] for style in reply_styles1], k=1 )[0] - reply_styles2 = [ - ("不要回复的太有条理,可以有个性", 0.6), # 60%概率 - ("不要回复的太有条理,可以复读", 0.15), # 15%概率 - ("回复的认真一些", 0.2), # 20%概率 - ("可以回复单个表情符号", 0.05), # 5%概率 + ("不要回复的太有条理,可以有个性", 0.6), + ("不要回复的太有条理,可以复读", 0.15), + ("回复的认真一些", 0.2), + ("可以回复单个表情符号", 0.05), ] reply_style2_chosen = random.choices( [style[0] for style in reply_styles2], weights=[style[1] for style in reply_styles2], k=1 )[0] - - # 调取记忆 memory_prompt = "" related_memory = await HippocampusManager.get_instance().get_memory_from_text( text=message_txt, max_memory_num=2, max_memory_length=2, max_depth=3, fast_retrieval=False @@ -309,23 +369,14 @@ class PromptBuilder: if related_memory: for memory in related_memory: related_memory_info += memory[1] - # memory_prompt = f"你想起你之前见过的事情:{related_memory_info}。\n以上是你的回忆,不一定是目前聊天里的人说的,也不一定是现在发生的事情,请记住。\n" memory_prompt = await global_prompt_manager.format_prompt( "memory_prompt", related_memory_info=related_memory_info ) - - # 获取聊天上下文 - if chat_stream.group_info: - chat_in_group = True - else: - chat_in_group = False - message_list_before_now = get_raw_msg_before_timestamp_with_chat( chat_id=chat_stream.stream_id, timestamp=time.time(), limit=global_config.observation_context_size, ) - chat_talking_prompt = await build_readable_messages( message_list_before_now, replace_bot_name=True, @@ -369,13 +420,12 @@ class PromptBuilder: start_time = time.time() prompt_info = await self.get_prompt_info(message_txt, threshold=0.38) if prompt_info: - # prompt_info = f"""\n你有以下这些**知识**:\n{prompt_info}\n请你**记住上面的知识**,之后可能会用到。\n""" prompt_info = await global_prompt_manager.format_prompt("knowledge_prompt", prompt_info=prompt_info) end_time = time.time() logger.debug(f"知识检索耗时: {(end_time - start_time):.3f}秒") - logger.debug("开始构建prompt") + if global_config.ENABLE_SCHEDULE_GEN: schedule_prompt = await global_prompt_manager.format_prompt( @@ -384,33 +434,60 @@ class PromptBuilder: else: schedule_prompt = "" - prompt = await global_prompt_manager.format_prompt( - "reasoning_prompt_main", - relation_prompt=relation_prompt, - sender_name=sender_name, - memory_prompt=memory_prompt, - prompt_info=prompt_info, - schedule_prompt=schedule_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_target_2=await global_prompt_manager.get_prompt_async("chat_target_group2") - if chat_in_group - else await global_prompt_manager.get_prompt_async("chat_target_private2"), - chat_talking_prompt=chat_talking_prompt, - message_txt=message_txt, - bot_name=global_config.BOT_NICKNAME, - bot_other_names="/".join( - global_config.BOT_ALIAS_NAMES, - ), - prompt_personality=prompt_personality, - mood_prompt=mood_prompt, - reply_style1=reply_style1_chosen, - reply_style2=reply_style2_chosen, - keywords_reaction_prompt=keywords_reaction_prompt, - prompt_ger=prompt_ger, - moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), - ) + logger.debug("开始构建 normal prompt") + + # --- Choose template and format based on chat type --- + if is_group_chat: + template_name = "reasoning_prompt_main" + effective_sender_name = sender_name + chat_target_1 = await global_prompt_manager.get_prompt_async("chat_target_group1") + chat_target_2 = await global_prompt_manager.get_prompt_async("chat_target_group2") + + prompt = await global_prompt_manager.format_prompt( + template_name, + relation_prompt=relation_prompt, + sender_name=effective_sender_name, + memory_prompt=memory_prompt, + prompt_info=prompt_info, + schedule_prompt=schedule_prompt, + chat_target=chat_target_1, + chat_target_2=chat_target_2, + chat_talking_prompt=chat_talking_prompt, + message_txt=message_txt, + bot_name=global_config.BOT_NICKNAME, + bot_other_names="/".join(global_config.BOT_ALIAS_NAMES), + prompt_personality=prompt_personality, + mood_prompt=mood_prompt, + reply_style1=reply_style1_chosen, + reply_style2=reply_style2_chosen, + keywords_reaction_prompt=keywords_reaction_prompt, + prompt_ger=prompt_ger, + moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), + ) + else: + template_name = "reasoning_prompt_private_main" + effective_sender_name = sender_name + + prompt = await global_prompt_manager.format_prompt( + template_name, + relation_prompt=relation_prompt, + sender_name=effective_sender_name, + memory_prompt=memory_prompt, + prompt_info=prompt_info, + schedule_prompt=schedule_prompt, + chat_talking_prompt=chat_talking_prompt, + message_txt=message_txt, + bot_name=global_config.BOT_NICKNAME, + bot_other_names="/".join(global_config.BOT_ALIAS_NAMES), + prompt_personality=prompt_personality, + mood_prompt=mood_prompt, + reply_style1=reply_style1_chosen, + reply_style2=reply_style2_chosen, + keywords_reaction_prompt=keywords_reaction_prompt, + prompt_ger=prompt_ger, + moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), + ) + # --- End choosing template --- return prompt @@ -670,6 +747,110 @@ class PromptBuilder: # 返回所有找到的内容,用换行分隔 return "\n".join(str(result["content"]) for result in results) + async def build_planner_prompt( + self, + is_group_chat: bool, # Now passed as argument + chat_target_info: Optional[dict], # Now passed as argument + cycle_history: Deque["CycleInfo"], # Now passed as argument (Type hint needs import or string) + observed_messages_str: str, + current_mind: Optional[str], + structured_info: Dict[str, Any], + current_available_actions: Dict[str, str], + # replan_prompt: str, # Replan logic still simplified + ) -> str: + """构建 Planner LLM 的提示词 (获取模板并填充数据)""" + try: + # --- Determine chat context --- + chat_context_description = "你现在正在一个群聊中" + chat_target_name = None # Only relevant for private + if not is_group_chat and chat_target_info: + chat_target_name = chat_target_info.get('person_name') or chat_target_info.get('user_nickname') or "对方" + chat_context_description = f"你正在和 {chat_target_name} 私聊" + # --- End determining chat context --- + + # ... (Copy logic from HeartFChatting._build_planner_prompt here) ... + # Structured info block + structured_info_block = "" + if structured_info: + structured_info_block = f"以下是一些额外的信息:\n{structured_info}\n" + + # Chat content block + chat_content_block = "" + if observed_messages_str: + # Use triple quotes for multi-line string literal + chat_content_block = f"""观察到的最新聊天内容如下: +--- +{observed_messages_str} +---""" + else: + chat_content_block = "当前没有观察到新的聊天内容。\\n" + + # Current mind block + current_mind_block = "" + if current_mind: + current_mind_block = f"你的内心想法:\n{current_mind}" + else: + current_mind_block = "你的内心想法:\n[没有特别的想法]" + + # Cycle info block (using passed cycle_history) + cycle_info_block = "" + recent_active_cycles = [] + for cycle in reversed(cycle_history): + if cycle.action_taken: + recent_active_cycles.append(cycle) + if len(recent_active_cycles) == 3: + break + consecutive_text_replies = 0 + responses_for_prompt = [] + for cycle in recent_active_cycles: + if cycle.action_type == "text_reply": + consecutive_text_replies += 1 + response_text = cycle.response_info.get("response_text", []) + formatted_response = "[空回复]" if not response_text else " ".join(response_text) + responses_for_prompt.append(formatted_response) + else: + break + if consecutive_text_replies >= 3: + cycle_info_block = f'你已经连续回复了三条消息(最近: "{responses_for_prompt[0]}",第二近: "{responses_for_prompt[1]}",第三近: "{responses_for_prompt[2]}")。你回复的有点多了,请注意' + elif consecutive_text_replies == 2: + cycle_info_block = f'你已经连续回复了两条消息(最近: "{responses_for_prompt[0]}",第二近: "{responses_for_prompt[1]}"),请注意' + elif consecutive_text_replies == 1: + cycle_info_block = f'你刚刚已经回复一条消息(内容: "{responses_for_prompt[0]}")' + if cycle_info_block: + cycle_info_block = f"\n【近期回复历史】\n{cycle_info_block}\n" + else: + cycle_info_block = "\n【近期回复历史】\n(最近没有连续文本回复)\n" + + individuality = Individuality.get_instance() + prompt_personality = individuality.get_prompt(x_person=2, level=2) + + action_options_text = "当前你可以选择的行动有:\n" + action_keys = list(current_available_actions.keys()) + for name in action_keys: + desc = current_available_actions[name] + action_options_text += f"- '{name}': {desc}\n" + example_action_key = action_keys[0] if action_keys else "no_reply" + + planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt") + + prompt = planner_prompt_template.format( + bot_name=global_config.BOT_NICKNAME, + prompt_personality=prompt_personality, + chat_context_description=chat_context_description, + structured_info_block=structured_info_block, + chat_content_block=chat_content_block, + current_mind_block=current_mind_block, + cycle_info_block=cycle_info_block, + action_options_text=action_options_text, + example_action=example_action_key, + ) + return prompt + + except Exception as e: + logger.error(f"[PromptBuilder] 构建 Planner 提示词时出错: {e}") + logger.error(traceback.format_exc()) + return "[构建 Planner Prompt 时出错]" + init_prompt() prompt_builder = PromptBuilder() From c4a7b842f66ac10e87ce0bca4872ae940ba8c34d Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 1 May 2025 22:50:06 +0800 Subject: [PATCH 30/67] =?UTF-8?q?fix=EF=BC=9A=E5=BE=AE=E6=B7=98Pomrpt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/heart_flow/sub_mind.py | 3 ++- src/heart_flow/utils_chat.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index 3aa1c11d..ca774f1d 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -56,7 +56,8 @@ def init_prompt(): {chat_observe_info} 你现在{mood_info} -请仔细阅读聊天内容,分析你和 {chat_target_name} 的关系,分析你刚刚发言和对方的反应,思考你要不要回复。然后思考你是否需要使用函数工具。 +请仔细阅读聊天内容,想想你和 {chat_target_name} 的关系,回顾你们刚刚的交流,你刚刚发言和对方的反应,思考聊天的主题。 +请思考你要不要回复以及如何回复对方。然后思考你是否需要使用函数工具。 思考并输出你的内心想法 输出要求: 1. 根据聊天内容生成你的想法,{hf_do_next} diff --git a/src/heart_flow/utils_chat.py b/src/heart_flow/utils_chat.py index fcbcc738..260fc16d 100644 --- a/src/heart_flow/utils_chat.py +++ b/src/heart_flow/utils_chat.py @@ -51,8 +51,8 @@ async def get_chat_type_and_target_info(chat_id: str) -> Tuple[bool, Optional[Di person_id = await asyncio.to_thread(person_info_manager.get_person_id, platform, user_id) person_name = None if person_id: - # get_value is async, so await it directly - person_name = await person_info_manager.get_value(person_id, "person_name") + # get_value is async, so await it directly + person_name = await person_info_manager.get_value(person_id, "person_name") target_info['person_id'] = person_id target_info['person_name'] = person_name From 2669572b301460db3bb20258c2292f4125a95fc7 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 1 May 2025 22:50:29 +0800 Subject: [PATCH 31/67] r --- src/heart_flow/background_tasks.py | 10 +-- src/heart_flow/observation.py | 40 +++++---- src/heart_flow/sub_heartflow.py | 23 ++--- src/heart_flow/sub_mind.py | 32 +++---- src/heart_flow/subheartflow_manager.py | 85 ++++++++++-------- src/heart_flow/utils_chat.py | 43 ++++----- src/plugins/heartFC_chat/heartFC_chat.py | 63 +++++++------ .../heartFC_chat/heartflow_prompt_builder.py | 88 +++++++++---------- src/plugins/heartFC_chat/normal_chat.py | 24 ++--- 9 files changed, 220 insertions(+), 188 deletions(-) diff --git a/src/heart_flow/background_tasks.py b/src/heart_flow/background_tasks.py index 99816540..c1730d5a 100644 --- a/src/heart_flow/background_tasks.py +++ b/src/heart_flow/background_tasks.py @@ -20,7 +20,7 @@ NORMAL_CHAT_TIMEOUT_CHECK_INTERVAL_SECONDS = 60 # 新增状态评估间隔 HF_JUDGE_STATE_UPDATE_INTERVAL_SECONDS = 60 # 新增私聊激活检查间隔 -PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS = 5 # 与兴趣评估类似,设为5秒 +PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS = 5 # 与兴趣评估类似,设为5秒 CLEANUP_INTERVAL_SECONDS = 1200 STATE_UPDATE_INTERVAL_SECONDS = 60 @@ -76,7 +76,7 @@ class BackgroundTaskManager: self._normal_chat_timeout_check_task: Optional[asyncio.Task] = None self._hf_judge_state_update_task: Optional[asyncio.Task] = None self._into_focus_task: Optional[asyncio.Task] = None - self._private_chat_activation_task: Optional[asyncio.Task] = None # 新增私聊激活任务引用 + self._private_chat_activation_task: Optional[asyncio.Task] = None # 新增私聊激活任务引用 self._tasks: List[Optional[asyncio.Task]] = [] # Keep track of all tasks async def start_tasks(self): @@ -129,9 +129,9 @@ class BackgroundTaskManager: ), # 新增私聊激活任务配置 ( - # Use lambda to pass the interval to the runner function + # Use lambda to pass the interval to the runner function lambda: self._run_private_chat_activation_cycle(PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS), - "debug", + "debug", f"私聊激活检查任务已启动 间隔:{PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS}s", "_private_chat_activation_task", ), @@ -294,5 +294,5 @@ class BackgroundTaskManager: await _run_periodic_loop( task_name="Private Chat Activation Check", interval=interval, - task_func=self.subheartflow_manager.sbhf_absent_private_into_focus + task_func=self.subheartflow_manager.sbhf_absent_private_into_focus, ) diff --git a/src/heart_flow/observation.py b/src/heart_flow/observation.py index 06dcdc31..0d51c938 100644 --- a/src/heart_flow/observation.py +++ b/src/heart_flow/observation.py @@ -13,11 +13,10 @@ from src.plugins.utils.chat_message_builder import ( get_person_id_list, ) from src.plugins.utils.prompt_builder import Prompt, global_prompt_manager -from src.plugins.chat.chat_stream import chat_manager from typing import Optional -from src.plugins.person_info.person_info import person_info_manager + # Import the new utility function -from .utils_chat import get_chat_type_and_target_info +from .utils_chat import get_chat_type_and_target_info logger = get_logger("observation") @@ -26,14 +25,14 @@ Prompt( """这是qq群聊的聊天记录,请总结以下聊天记录的主题: {chat_logs} 请用一句话概括,包括人物、事件和主要信息,不要分点。""", - "chat_summary_group_prompt" # Template for group chat + "chat_summary_group_prompt", # Template for group chat ) Prompt( """这是你和{chat_target}的私聊记录,请总结以下聊天记录的主题: {chat_logs} 请用一句话概括,包括事件,时间,和主要信息,不要分点。""", - "chat_summary_private_prompt" # Template for private chat + "chat_summary_private_prompt", # Template for private chat ) # --- End Prompt Template Definition --- @@ -56,9 +55,9 @@ class ChattingObservation(Observation): super().__init__("chat", chat_id) self.chat_id = chat_id - # --- Initialize attributes (defaults) --- + # --- Initialize attributes (defaults) --- self.is_group_chat: bool = False - self.chat_target_info: Optional[dict] = None + self.chat_target_info: Optional[dict] = None # --- End Initialization --- # --- Other attributes initialized in __init__ --- @@ -77,11 +76,12 @@ class ChattingObservation(Observation): model=global_config.llm_observation, temperature=0.7, max_tokens=300, request_type="chat_observation" ) - async def initialize(self): # --- Use utility function to determine chat type and fetch info --- self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.chat_id) - logger.debug(f"ChattingObservation {self.chat_id} initialized: is_group={self.is_group_chat}, target_info={self.chat_target_info}") + logger.debug( + f"ChattingObservation {self.chat_id} initialized: is_group={self.is_group_chat}, target_info={self.chat_target_info}" + ) # --- End using utility function --- # Fetch initial messages (existing logic) @@ -141,30 +141,33 @@ class ChattingObservation(Observation): ) # --- Build prompt using template --- - prompt = None # Initialize prompt as None + prompt = None # Initialize prompt as None try: # 构建 Prompt - 根据 is_group_chat 选择模板 if self.is_group_chat: prompt_template_name = "chat_summary_group_prompt" prompt = await global_prompt_manager.format_prompt( - prompt_template_name, - chat_logs=oldest_messages_str + prompt_template_name, chat_logs=oldest_messages_str ) else: # For private chat, add chat_target to the prompt variables prompt_template_name = "chat_summary_private_prompt" # Determine the target name for the prompt - chat_target_name = "对方" # Default fallback + chat_target_name = "对方" # Default fallback if self.chat_target_info: # Prioritize person_name, then nickname - chat_target_name = self.chat_target_info.get('person_name') or self.chat_target_info.get('user_nickname') or chat_target_name - + chat_target_name = ( + self.chat_target_info.get("person_name") + or self.chat_target_info.get("user_nickname") + or chat_target_name + ) + # Format the private chat prompt prompt = await global_prompt_manager.format_prompt( prompt_template_name, # Assuming the private prompt template uses {chat_target} - chat_target=chat_target_name, - chat_logs=oldest_messages_str + chat_target=chat_target_name, + chat_logs=oldest_messages_str, ) except Exception as e: logger.error(f"构建总结 Prompt 失败 for chat {self.chat_id}: {e}") @@ -172,7 +175,7 @@ class ChattingObservation(Observation): summary = "没有主题的闲聊" # 默认值 - if prompt: # Check if prompt was built successfully + if prompt: # Check if prompt was built successfully try: summary_result, _, _ = await self.llm_summary.generate_response(prompt) if summary_result: # 确保结果不为空 @@ -183,7 +186,6 @@ class ChattingObservation(Observation): else: logger.warning(f"因 Prompt 构建失败,跳过 LLM 总结 for chat {self.chat_id}") - mid_memory = { "id": str(int(datetime.now().timestamp())), "theme": summary, diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index 8e00c263..eb8bbabd 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -13,7 +13,6 @@ from src.plugins.heartFC_chat.normal_chat import NormalChat 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 -from src.plugins.person_info.person_info import person_info_manager from .utils_chat import get_chat_type_and_target_info @@ -240,9 +239,9 @@ class SubHeartflow: self.chat_state_last_time: float = 0 self.history_chat_state: List[Tuple[ChatState, float]] = [] - # --- Initialize attributes --- + # --- Initialize attributes --- self.is_group_chat: bool = False - self.chat_target_info: Optional[dict] = None + self.chat_target_info: Optional[dict] = None # --- End Initialization --- # 兴趣检测器 @@ -268,16 +267,20 @@ class SubHeartflow: ) # 日志前缀 - Moved determination to initialize - self.log_prefix = str(subheartflow_id) # Initial default prefix + self.log_prefix = str(subheartflow_id) # Initial default prefix async def initialize(self): """异步初始化方法,创建兴趣流并确定聊天类型""" - + # --- Use utility function to determine chat type and fetch info --- self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.chat_id) # Update log prefix after getting info (potential stream name) - self.log_prefix = chat_manager.get_stream_name(self.subheartflow_id) or self.subheartflow_id # Keep this line or adjust if utils provides name - logger.debug(f"SubHeartflow {self.chat_id} initialized: is_group={self.is_group_chat}, target_info={self.chat_target_info}") + self.log_prefix = ( + chat_manager.get_stream_name(self.subheartflow_id) or self.subheartflow_id + ) # Keep this line or adjust if utils provides name + logger.debug( + f"SubHeartflow {self.chat_id} initialized: is_group={self.is_group_chat}, target_info={self.chat_target_info}" + ) # --- End using utility function --- # Initialize interest system (existing logic) @@ -315,15 +318,15 @@ class SubHeartflow: if not chat_stream: logger.error(f"{log_prefix} 无法获取 chat_stream,无法启动 NormalChat。") return False - + self.normal_chat_instance = NormalChat(chat_stream=chat_stream, interest_dict=self.get_interest_dict()) - + # 进行异步初始化 await self.normal_chat_instance.initialize() # 启动聊天任务 logger.info(f"{log_prefix} 开始普通聊天,随便水群...") - await self.normal_chat_instance.start_chat() # start_chat now ensures init is called again if needed + await self.normal_chat_instance.start_chat() # start_chat now ensures init is called again if needed return True except Exception as e: logger.error(f"{log_prefix} 启动 NormalChat 或其初始化时出错: {e}") diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index ca774f1d..4ce2fe41 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -20,7 +20,7 @@ logger = get_logger("sub_heartflow") def init_prompt(): - # --- Group Chat Prompt --- + # --- Group Chat Prompt --- group_prompt = """ {extra_info} {relation_prompt} @@ -45,7 +45,7 @@ def init_prompt(): 3. 如需处理消息或回复,请使用工具。""" Prompt(group_prompt, "sub_heartflow_prompt_before") - # --- Private Chat Prompt --- + # --- Private Chat Prompt --- private_prompt = """ {extra_info} {relation_prompt} @@ -69,9 +69,9 @@ def init_prompt(): 1. 输出想法后考虑是否需要使用工具 2. 工具可获取信息或执行操作 3. 如需处理消息或回复,请使用工具。""" - Prompt(private_prompt, "sub_heartflow_prompt_private_before") # New template name + Prompt(private_prompt, "sub_heartflow_prompt_private_before") # New template name - # --- Last Loop Prompt (remains the same) --- + # --- Last Loop Prompt (remains the same) --- last_loop_t = """ 刚刚你的内心想法是:{current_thinking_info} {if_replan_prompt} @@ -152,17 +152,19 @@ class SubMind: # 获取观察对象 observation = self.observations[0] if self.observations else None - if not observation or not hasattr(observation, 'is_group_chat'): # Ensure it's ChattingObservation or similar + if not observation or not hasattr(observation, "is_group_chat"): # Ensure it's ChattingObservation or similar logger.error(f"{self.log_prefix} 无法获取有效的观察对象或缺少聊天类型信息") self.update_current_mind("(观察出错了...)") return self.current_mind, self.past_mind - + is_group_chat = observation.is_group_chat chat_target_info = observation.chat_target_info - chat_target_name = "对方" # Default for private + chat_target_name = "对方" # Default for private if not is_group_chat and chat_target_info: - chat_target_name = chat_target_info.get('person_name') or chat_target_info.get('user_nickname') or chat_target_name - # --- End getting observation info --- + chat_target_name = ( + chat_target_info.get("person_name") or chat_target_info.get("user_nickname") or chat_target_name + ) + # --- End getting observation info --- # 获取观察内容 chat_observe_info = observation.get_observe_info() @@ -274,7 +276,7 @@ class SubMind: )[0] # ---------- 4. 构建最终提示词 ---------- - # --- Choose template based on chat type --- + # --- Choose template based on chat type --- if is_group_chat: template_name = "sub_heartflow_prompt_before" prompt = (await global_prompt_manager.get_prompt_async(template_name)).format( @@ -290,22 +292,22 @@ class SubMind: cycle_info_block=cycle_info_block, # chat_target_name is not used in group prompt ) - else: # Private chat + else: # Private chat template_name = "sub_heartflow_prompt_private_before" prompt = (await global_prompt_manager.get_prompt_async(template_name)).format( - extra_info="", + extra_info="", prompt_personality=prompt_personality, - relation_prompt=relation_prompt, # Might need adjustment for private context + relation_prompt=relation_prompt, # Might need adjustment for private context bot_name=individuality.name, time_now=time_now, - chat_target_name=chat_target_name, # Pass target name + chat_target_name=chat_target_name, # Pass target name chat_observe_info=chat_observe_info, mood_info=mood_info, hf_do_next=hf_do_next, last_loop_prompt=last_loop_prompt, cycle_info_block=cycle_info_block, ) - # --- End choosing template --- + # --- End choosing template --- # ---------- 5. 执行LLM请求并处理响应 ---------- content = "" # 初始化内容变量 diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index 25e5d3cc..04153abf 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -345,7 +345,8 @@ class SubHeartflowManager: async with self._lock: # 1. 筛选出所有 ABSENT 状态的 *群聊* 子心流 absent_group_subflows = [ - hf for hf in self.subheartflows.values() + hf + for hf in self.subheartflows.values() if hf.chat_state.chat_status == ChatState.ABSENT and hf.is_group_chat ] @@ -358,7 +359,7 @@ class SubHeartflowManager: flow_id = sub_hf_to_evaluate.subheartflow_id stream_name = chat_manager.get_stream_name(flow_id) or flow_id log_prefix = f"[{stream_name}]" - + # --- Private chat check (redundant due to filter above, but safe) --- # if not sub_hf_to_evaluate.is_group_chat: # logger.debug(f"{log_prefix} 是私聊,跳过 CHAT 状态评估。") @@ -684,24 +685,26 @@ class SubHeartflowManager: current_state = subflow.chat_state.chat_status if current_state == ChatState.FOCUSED: - target_state = ChatState.ABSENT # Default target + target_state = ChatState.ABSENT # Default target log_reason = "默认转换 (私聊或群聊)" # --- Modify logic based on chat type --- # if subflow.is_group_chat: # Group chat: Decide between ABSENT or CHAT - if random.random() < 0.5: # 50% chance to try CHAT + if random.random() < 0.5: # 50% chance to try CHAT current_mai_state = self.mai_state_info.get_current_state() chat_limit = current_mai_state.get_normal_chat_max_num() current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT) - + if current_chat_count < chat_limit: target_state = ChatState.CHAT log_reason = f"群聊随机选择 CHAT (当前 {current_chat_count}/{chat_limit})" else: - target_state = ChatState.ABSENT # Fallback to ABSENT if CHAT limit reached - log_reason = f"群聊随机选择 CHAT 但已达上限 ({current_chat_count}/{chat_limit}),转为 ABSENT" - else: # 50% chance to go directly to ABSENT + target_state = ChatState.ABSENT # Fallback to ABSENT if CHAT limit reached + log_reason = ( + f"群聊随机选择 CHAT 但已达上限 ({current_chat_count}/{chat_limit}),转为 ABSENT" + ) + else: # 50% chance to go directly to ABSENT target_state = ChatState.ABSENT log_reason = "群聊随机选择 ABSENT" else: @@ -732,6 +735,7 @@ class SubHeartflowManager: logger.warning( f"[状态转换请求] 收到对 {stream_name} 的请求,但其状态为 {current_state.value} (非 FOCUSED),不执行转换" ) + # --- 结束新增 --- # # --- 新增:处理私聊从 ABSENT 直接到 FOCUSED 的逻辑 --- # @@ -740,17 +744,17 @@ class SubHeartflowManager: log_prefix_task = "[私聊激活检查]" transitioned_count = 0 checked_count = 0 - + # --- 获取当前状态和 FOCUSED 上限 --- # current_mai_state = self.mai_state_info.get_current_state() focused_limit = current_mai_state.get_focused_chat_max_num() # --- 检查是否允许 FOCUS 模式 --- # if not global_config.allow_focus_mode: - # Log less frequently to avoid spam - # if int(time.time()) % 60 == 0: - # logger.debug(f"{log_prefix_task} 配置不允许进入 FOCUSED 状态") - return + # Log less frequently to avoid spam + # if int(time.time()) % 60 == 0: + # logger.debug(f"{log_prefix_task} 配置不允许进入 FOCUSED 状态") + return if focused_limit <= 0: # logger.debug(f"{log_prefix_task} 当前状态 ({current_mai_state.value}) 不允许 FOCUSED 子心流") @@ -759,10 +763,11 @@ class SubHeartflowManager: async with self._lock: # --- 获取当前 FOCUSED 计数 (不上锁版本) --- # current_focused_count = self.count_subflows_by_state_nolock(ChatState.FOCUSED) - + # --- 筛选出所有 ABSENT 状态的私聊子心流 --- # eligible_subflows = [ - hf for hf in self.subheartflows.values() + hf + for hf in self.subheartflows.values() if hf.chat_state.chat_status == ChatState.ABSENT and not hf.is_group_chat ] checked_count = len(eligible_subflows) @@ -775,8 +780,10 @@ class SubHeartflowManager: for sub_hf in eligible_subflows: # --- 再次检查 FOCUSED 上限,因为可能有多个同时激活 --- # if current_focused_count >= focused_limit: - logger.debug(f"{log_prefix_task} 已达专注上限 ({current_focused_count}/{focused_limit}),停止检查后续私聊。") - break # 已满,无需再检查其他私聊 + logger.debug( + f"{log_prefix_task} 已达专注上限 ({current_focused_count}/{focused_limit}),停止检查后续私聊。" + ) + break # 已满,无需再检查其他私聊 flow_id = sub_hf.subheartflow_id stream_name = chat_manager.get_stream_name(flow_id) or flow_id @@ -784,45 +791,51 @@ class SubHeartflowManager: try: # --- 检查是否有新活动 --- # - observation = sub_hf._get_primary_observation() # 获取主要观察者 + observation = sub_hf._get_primary_observation() # 获取主要观察者 is_active = False if observation: - # 检查自上次状态变为 ABSENT 后是否有新消息 - # 使用 chat_state_changed_time 可能更精确 - # 加一点点缓冲时间(例如 1 秒)以防时间戳完全相等 - timestamp_to_check = sub_hf.chat_state_changed_time - 1 + # 检查自上次状态变为 ABSENT 后是否有新消息 + # 使用 chat_state_changed_time 可能更精确 + # 加一点点缓冲时间(例如 1 秒)以防时间戳完全相等 + timestamp_to_check = sub_hf.chat_state_changed_time - 1 has_new = await observation.has_new_messages_since(timestamp_to_check) if has_new: - is_active = True - logger.debug(f"{log_prefix} 检测到新消息,标记为活跃。") - # 可选:检查兴趣度是否大于0 (如果需要) - # interest_level = await sub_hf.interest_chatting.get_interest() - # if interest_level > 0: - # is_active = True - # logger.debug(f"{log_prefix} 检测到兴趣度 > 0 ({interest_level:.2f}),标记为活跃。") + is_active = True + logger.debug(f"{log_prefix} 检测到新消息,标记为活跃。") + # 可选:检查兴趣度是否大于0 (如果需要) + # interest_level = await sub_hf.interest_chatting.get_interest() + # if interest_level > 0: + # is_active = True + # logger.debug(f"{log_prefix} 检测到兴趣度 > 0 ({interest_level:.2f}),标记为活跃。") else: - logger.warning(f"{log_prefix} 无法获取主要观察者来检查活动状态。") + logger.warning(f"{log_prefix} 无法获取主要观察者来检查活动状态。") # --- 如果活跃且未达上限,则尝试转换 --- # if is_active: - logger.info(f"{log_prefix} 检测到活跃且未达专注上限 ({current_focused_count}/{focused_limit}),尝试转换为 FOCUSED。") + logger.info( + f"{log_prefix} 检测到活跃且未达专注上限 ({current_focused_count}/{focused_limit}),尝试转换为 FOCUSED。" + ) await sub_hf.change_chat_state(ChatState.FOCUSED) # 确认转换成功 if sub_hf.chat_state.chat_status == ChatState.FOCUSED: transitioned_count += 1 - current_focused_count += 1 # 更新计数器以供本轮后续检查 + current_focused_count += 1 # 更新计数器以供本轮后续检查 logger.info(f"{log_prefix} 成功进入 FOCUSED 状态。") else: - logger.warning(f"{log_prefix} 尝试进入 FOCUSED 状态失败。当前状态: {sub_hf.chat_state.chat_status.value}") + logger.warning( + f"{log_prefix} 尝试进入 FOCUSED 状态失败。当前状态: {sub_hf.chat_state.chat_status.value}" + ) # else: # 不活跃,无需操作 # logger.debug(f"{log_prefix} 未检测到新活动,保持 ABSENT。") - + except Exception as e: logger.error(f"{log_prefix} 检查私聊活动或转换状态时出错: {e}", exc_info=True) - + # --- 循环结束后记录总结日志 --- # if transitioned_count > 0: - logger.debug(f"{log_prefix_task} 完成,共检查 {checked_count} 个私聊,{transitioned_count} 个转换为 FOCUSED。") + logger.debug( + f"{log_prefix_task} 完成,共检查 {checked_count} 个私聊,{transitioned_count} 个转换为 FOCUSED。" + ) # --- 结束新增 --- # diff --git a/src/heart_flow/utils_chat.py b/src/heart_flow/utils_chat.py index 260fc16d..3e082b6d 100644 --- a/src/heart_flow/utils_chat.py +++ b/src/heart_flow/utils_chat.py @@ -6,6 +6,7 @@ from src.plugins.person_info.person_info import person_info_manager logger = get_logger("heartflow_utils") + async def get_chat_type_and_target_info(chat_id: str) -> Tuple[bool, Optional[Dict]]: """ 获取聊天类型(是否群聊)和私聊对象信息。 @@ -14,7 +15,7 @@ async def get_chat_type_and_target_info(chat_id: str) -> Tuple[bool, Optional[Di chat_id: 聊天流ID Returns: - Tuple[bool, Optional[Dict]]: + Tuple[bool, Optional[Dict]]: - bool: 是否为群聊 (True 是群聊, False 是私聊或未知) - Optional[Dict]: 如果是私聊,包含对方信息的字典;否则为 None。 字典包含: platform, user_id, user_nickname, person_id, person_name @@ -23,29 +24,29 @@ async def get_chat_type_and_target_info(chat_id: str) -> Tuple[bool, Optional[Di chat_target_info = None try: - chat_stream = await asyncio.to_thread(chat_manager.get_stream, chat_id) # Use to_thread if get_stream is sync + chat_stream = await asyncio.to_thread(chat_manager.get_stream, chat_id) # Use to_thread if get_stream is sync # If get_stream is already async, just use: chat_stream = await chat_manager.get_stream(chat_id) - + if chat_stream: if chat_stream.group_info: is_group_chat = True - chat_target_info = None # Explicitly None for group chat - elif chat_stream.user_info: # It's a private chat + chat_target_info = None # Explicitly None for group chat + elif chat_stream.user_info: # It's a private chat is_group_chat = False user_info = chat_stream.user_info platform = chat_stream.platform user_id = user_info.user_id - + # Initialize target_info with basic info target_info = { - 'platform': platform, - 'user_id': user_id, - 'user_nickname': user_info.user_nickname, - 'person_id': None, - 'person_name': None + "platform": platform, + "user_id": user_id, + "user_nickname": user_info.user_nickname, + "person_id": None, + "person_name": None, } - - # Try to fetch person info + + # Try to fetch person info try: # Assume get_person_id is sync (as per original code), keep using to_thread person_id = await asyncio.to_thread(person_info_manager.get_person_id, platform, user_id) @@ -54,18 +55,20 @@ async def get_chat_type_and_target_info(chat_id: str) -> Tuple[bool, Optional[Di # get_value is async, so await it directly person_name = await person_info_manager.get_value(person_id, "person_name") - target_info['person_id'] = person_id - target_info['person_name'] = person_name + target_info["person_id"] = person_id + target_info["person_name"] = person_name except Exception as person_e: - logger.warning(f"获取 person_id 或 person_name 时出错 for {platform}:{user_id} in utils: {person_e}") - - chat_target_info = target_info + logger.warning( + f"获取 person_id 或 person_name 时出错 for {platform}:{user_id} in utils: {person_e}" + ) + + chat_target_info = target_info else: logger.warning(f"无法获取 chat_stream for {chat_id} in utils") # Keep defaults: is_group_chat=False, chat_target_info=None - + except Exception as e: logger.error(f"获取聊天类型和目标信息时出错 for {chat_id}: {e}", exc_info=True) # Keep defaults on error - return is_group_chat, chat_target_info \ No newline at end of file + return is_group_chat, chat_target_info diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 260fb440..dd078f7c 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -26,7 +26,6 @@ from .heartFC_sender import HeartFCSender from src.plugins.chat.utils import process_llm_response from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager from src.plugins.moods.moods import MoodManager -from src.individuality.individuality import Individuality from src.heart_flow.utils_chat import get_chat_type_and_target_info @@ -197,9 +196,9 @@ class HeartFChatting: # 日志前缀 self.log_prefix: str = str(chat_id) # Initial default, will be updated - # --- Initialize attributes (defaults) --- + # --- Initialize attributes (defaults) --- self.is_group_chat: bool = False - self.chat_target_info: Optional[dict] = None + self.chat_target_info: Optional[dict] = None # --- End Initialization --- # 动作管理器 @@ -244,26 +243,30 @@ class HeartFChatting: """ if self._initialized: return True - + # --- Use utility function to determine chat type and fetch info --- # Note: get_chat_type_and_target_info handles getting the chat_stream internally self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.stream_id) - + # Update log prefix based on potential stream name (if needed, or get it from chat_stream if util doesn't return it) # Assuming get_chat_type_and_target_info focuses only on type/target # We still need the chat_stream object itself for other operations try: self.chat_stream = await asyncio.to_thread(chat_manager.get_stream, self.stream_id) if not self.chat_stream: - logger.error(f"[HFC:{self.stream_id}] 获取ChatStream失败 during _initialize, though util func might have succeeded earlier.") - return False # Cannot proceed without chat_stream object + logger.error( + f"[HFC:{self.stream_id}] 获取ChatStream失败 during _initialize, though util func might have succeeded earlier." + ) + return False # Cannot proceed without chat_stream object # Update log prefix using the fetched stream object self.log_prefix = f"[{chat_manager.get_stream_name(self.stream_id) or self.stream_id}]" except Exception as e: - logger.error(f"[HFC:{self.stream_id}] 获取ChatStream时出错 in _initialize: {e}") - return False - - logger.debug(f"{self.log_prefix} HeartFChatting initialized: is_group={self.is_group_chat}, target_info={self.chat_target_info}") + logger.error(f"[HFC:{self.stream_id}] 获取ChatStream时出错 in _initialize: {e}") + return False + + logger.debug( + f"{self.log_prefix} HeartFChatting initialized: is_group={self.is_group_chat}, target_info={self.chat_target_info}" + ) # --- End using utility function --- self._initialized = True @@ -853,13 +856,13 @@ class HeartFChatting: # --- 构建提示词 (调用修改后的 PromptBuilder 方法) --- prompt = await prompt_builder.build_planner_prompt( - is_group_chat=self.is_group_chat, # <-- Pass HFC state - chat_target_info=self.chat_target_info, # <-- Pass HFC state - cycle_history=self._cycle_history, # <-- Pass HFC state - observed_messages_str=observed_messages_str, # <-- Pass local variable - current_mind=current_mind, # <-- Pass argument - structured_info=self.sub_mind.structured_info, # <-- Pass SubMind info - current_available_actions=current_available_actions # <-- Pass determined actions + is_group_chat=self.is_group_chat, # <-- Pass HFC state + chat_target_info=self.chat_target_info, # <-- Pass HFC state + cycle_history=self._cycle_history, # <-- Pass HFC state + observed_messages_str=observed_messages_str, # <-- Pass local variable + current_mind=current_mind, # <-- Pass argument + structured_info=self.sub_mind.structured_info, # <-- Pass SubMind info + current_available_actions=current_available_actions, # <-- Pass determined actions ) # --- 调用 LLM (普通文本生成) --- @@ -1279,25 +1282,29 @@ class HeartFChatting: # 2. 获取信息捕捉器 info_catcher = info_catcher_manager.get_info_catcher(thinking_id) - # --- Determine sender_name for private chat --- - sender_name_for_prompt = "某人" # Default for group or if info unavailable + # --- Determine sender_name for private chat --- + sender_name_for_prompt = "某人" # Default for group or if info unavailable if not self.is_group_chat and self.chat_target_info: # Prioritize person_name, then nickname - sender_name_for_prompt = self.chat_target_info.get('person_name') or self.chat_target_info.get('user_nickname') or sender_name_for_prompt - # --- End determining sender_name --- + sender_name_for_prompt = ( + self.chat_target_info.get("person_name") + or self.chat_target_info.get("user_nickname") + or sender_name_for_prompt + ) + # --- End determining sender_name --- # 3. 构建 Prompt with Timer("构建Prompt", {}): # 内部计时器,可选保留 prompt = await prompt_builder.build_prompt( build_mode="focus", - chat_stream=self.chat_stream, # Pass the stream object + chat_stream=self.chat_stream, # Pass the stream object # Focus specific args: reason=reason, current_mind_info=self.sub_mind.current_mind, structured_info=self.sub_mind.structured_info, - sender_name=sender_name_for_prompt, # Pass determined name + sender_name=sender_name_for_prompt, # Pass determined name # Normal specific args (not used in focus mode): - # message_txt="", + # message_txt="", ) # 4. 调用 LLM 生成回复 @@ -1305,9 +1312,9 @@ class HeartFChatting: reasoning_content = None model_name = "unknown_model" if not prompt: - logger.error(f"{self.log_prefix}[Replier-{thinking_id}] Prompt 构建失败,无法生成回复。") - return None - + logger.error(f"{self.log_prefix}[Replier-{thinking_id}] Prompt 构建失败,无法生成回复。") + return None + try: with Timer("LLM生成", {}): # 内部计时器,可选保留 content, reasoning_content, model_name = await self.model_normal.generate_response(prompt) diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 811b54d5..92346c93 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -138,7 +138,7 @@ JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query": Prompt("你现在正在做的事情是:{schedule_info}", "schedule_prompt") Prompt("\n你有以下这些**知识**:\n{prompt_info}\n请你**记住上面的知识**,之后可能会用到。\n", "knowledge_prompt") - # --- Template for HeartFChatting (FOCUSED mode) --- + # --- Template for HeartFChatting (FOCUSED mode) --- Prompt( """ {info_from_tools} @@ -157,10 +157,10 @@ JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query": 回复尽量简短一些。请注意把握聊天内容,{reply_style2}。{prompt_ger} {reply_style1},说中文,不要刻意突出自身学科背景,注意只输出回复内容。 {moderation_prompt}。注意:回复不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""", - "heart_flow_private_prompt", # New template for private FOCUSED chat + "heart_flow_private_prompt", # New template for private FOCUSED chat ) - # --- Template for NormalChat (CHAT mode) --- + # --- Template for NormalChat (CHAT mode) --- Prompt( """ {memory_prompt} @@ -179,17 +179,17 @@ JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query": 请注意不要输出多余内容(包括前后缀,冒号和引号,括号等),只输出回复内容。 {moderation_prompt} 不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。只输出回复内容""", - "reasoning_prompt_private_main", # New template for private CHAT chat + "reasoning_prompt_private_main", # New template for private CHAT chat ) async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_stream, sender_name) -> str: individuality = Individuality.get_instance() prompt_personality = individuality.get_prompt(x_person=0, level=2) - + # Determine if it's a group chat is_group_chat = bool(chat_stream.group_info) - + # Use sender_name passed from caller for private chat, otherwise use a default for group # Default sender_name for group chat isn't used in the group prompt template, but set for consistency effective_sender_name = sender_name if not is_group_chat else "某人" @@ -243,21 +243,21 @@ async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_s logger.debug("开始构建 focus prompt") - # --- Choose template based on chat type --- + # --- Choose template based on chat type --- if is_group_chat: template_name = "heart_flow_prompt" # Group specific formatting variables (already fetched or default) chat_target_1 = await global_prompt_manager.get_prompt_async("chat_target_group1") chat_target_2 = await global_prompt_manager.get_prompt_async("chat_target_group2") - + prompt = await global_prompt_manager.format_prompt( template_name, info_from_tools=structured_info_prompt, - chat_target=chat_target_1, # Used in group template + chat_target=chat_target_1, # Used in group template chat_talking_prompt=chat_talking_prompt, bot_name=global_config.BOT_NICKNAME, prompt_personality=prompt_personality, - chat_target_2=chat_target_2, # Used in group template + chat_target_2=chat_target_2, # Used in group template current_mind_info=current_mind_info, reply_style2=reply_style2_chosen, reply_style1=reply_style1_chosen, @@ -266,12 +266,12 @@ async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_s moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), # sender_name is not used in the group template ) - else: # Private chat + else: # Private chat template_name = "heart_flow_private_prompt" prompt = await global_prompt_manager.format_prompt( template_name, info_from_tools=structured_info_prompt, - sender_name=effective_sender_name, # Used in private template + sender_name=effective_sender_name, # Used in private template chat_talking_prompt=chat_talking_prompt, bot_name=global_config.BOT_NICKNAME, prompt_personality=prompt_personality, @@ -283,7 +283,7 @@ async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_s prompt_ger=prompt_ger, moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), ) - # --- End choosing template --- + # --- End choosing template --- logger.debug(f"focus_chat_prompt (is_group={is_group_chat}): \n{prompt}") return prompt @@ -302,10 +302,8 @@ class PromptBuilder: current_mind_info=None, structured_info=None, message_txt=None, - sender_name = "某人", + sender_name="某人", ) -> Optional[str]: - is_group_chat = bool(chat_stream.group_info) - if build_mode == "normal": return await self._build_prompt_normal(chat_stream, message_txt, sender_name) @@ -326,20 +324,22 @@ class PromptBuilder: who_chat_in_group = [] if is_group_chat: - who_chat_in_group = get_recent_group_speaker( - chat_stream.stream_id, - (chat_stream.user_info.platform, chat_stream.user_info.user_id) if chat_stream.user_info else None, - limit=global_config.observation_context_size, - ) + who_chat_in_group = get_recent_group_speaker( + chat_stream.stream_id, + (chat_stream.user_info.platform, chat_stream.user_info.user_id) if chat_stream.user_info else None, + limit=global_config.observation_context_size, + ) elif chat_stream.user_info: - who_chat_in_group.append((chat_stream.user_info.platform, chat_stream.user_info.user_id, chat_stream.user_info.user_nickname)) - + who_chat_in_group.append( + (chat_stream.user_info.platform, chat_stream.user_info.user_id, chat_stream.user_info.user_nickname) + ) + relation_prompt = "" for person in who_chat_in_group: if len(person) >= 3 and person[0] and person[1]: - relation_prompt += await relationship_manager.build_relationship_info(person) + relation_prompt += await relationship_manager.build_relationship_info(person) else: - logger.warning(f"Invalid person tuple encountered for relationship prompt: {person}") + logger.warning(f"Invalid person tuple encountered for relationship prompt: {person}") mood_manager = MoodManager.get_instance() mood_prompt = mood_manager.get_prompt() @@ -425,8 +425,6 @@ class PromptBuilder: end_time = time.time() logger.debug(f"知识检索耗时: {(end_time - start_time):.3f}秒") - - if global_config.ENABLE_SCHEDULE_GEN: schedule_prompt = await global_prompt_manager.format_prompt( "schedule_prompt", schedule_info=bot_schedule.get_current_num_task(num=1, time_info=False) @@ -435,14 +433,14 @@ class PromptBuilder: schedule_prompt = "" logger.debug("开始构建 normal prompt") - - # --- Choose template and format based on chat type --- + + # --- Choose template and format based on chat type --- if is_group_chat: template_name = "reasoning_prompt_main" - effective_sender_name = sender_name + effective_sender_name = sender_name chat_target_1 = await global_prompt_manager.get_prompt_async("chat_target_group1") chat_target_2 = await global_prompt_manager.get_prompt_async("chat_target_group2") - + prompt = await global_prompt_manager.format_prompt( template_name, relation_prompt=relation_prompt, @@ -466,8 +464,8 @@ class PromptBuilder: ) else: template_name = "reasoning_prompt_private_main" - effective_sender_name = sender_name - + effective_sender_name = sender_name + prompt = await global_prompt_manager.format_prompt( template_name, relation_prompt=relation_prompt, @@ -487,7 +485,7 @@ class PromptBuilder: prompt_ger=prompt_ger, moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), ) - # --- End choosing template --- + # --- End choosing template --- return prompt @@ -749,9 +747,9 @@ class PromptBuilder: async def build_planner_prompt( self, - is_group_chat: bool, # Now passed as argument - chat_target_info: Optional[dict], # Now passed as argument - cycle_history: Deque["CycleInfo"], # Now passed as argument (Type hint needs import or string) + is_group_chat: bool, # Now passed as argument + chat_target_info: Optional[dict], # Now passed as argument + cycle_history: Deque["CycleInfo"], # Now passed as argument (Type hint needs import or string) observed_messages_str: str, current_mind: Optional[str], structured_info: Dict[str, Any], @@ -760,20 +758,22 @@ class PromptBuilder: ) -> str: """构建 Planner LLM 的提示词 (获取模板并填充数据)""" try: - # --- Determine chat context --- + # --- Determine chat context --- chat_context_description = "你现在正在一个群聊中" - chat_target_name = None # Only relevant for private + chat_target_name = None # Only relevant for private if not is_group_chat and chat_target_info: - chat_target_name = chat_target_info.get('person_name') or chat_target_info.get('user_nickname') or "对方" + chat_target_name = ( + chat_target_info.get("person_name") or chat_target_info.get("user_nickname") or "对方" + ) chat_context_description = f"你正在和 {chat_target_name} 私聊" # --- End determining chat context --- - + # ... (Copy logic from HeartFChatting._build_planner_prompt here) ... # Structured info block structured_info_block = "" if structured_info: structured_info_block = f"以下是一些额外的信息:\n{structured_info}\n" - + # Chat content block chat_content_block = "" if observed_messages_str: @@ -784,14 +784,14 @@ class PromptBuilder: ---""" else: chat_content_block = "当前没有观察到新的聊天内容。\\n" - + # Current mind block current_mind_block = "" if current_mind: current_mind_block = f"你的内心想法:\n{current_mind}" else: current_mind_block = "你的内心想法:\n[没有特别的想法]" - + # Cycle info block (using passed cycle_history) cycle_info_block = "" recent_active_cycles = [] diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py index 3668e187..6b8fb9ce 100644 --- a/src/plugins/heartFC_chat/normal_chat.py +++ b/src/plugins/heartFC_chat/normal_chat.py @@ -33,14 +33,14 @@ class NormalChat: self.chat_stream = chat_stream self.stream_id = chat_stream.stream_id # Get initial stream name, might be updated in initialize - self.stream_name = chat_manager.get_stream_name(self.stream_id) or self.stream_id + self.stream_name = chat_manager.get_stream_name(self.stream_id) or self.stream_id # Interest dict self.interest_dict = interest_dict - # --- Initialize attributes (defaults) --- + # --- Initialize attributes (defaults) --- self.is_group_chat: bool = False - self.chat_target_info: Optional[dict] = None + self.chat_target_info: Optional[dict] = None # --- End Initialization --- # Other sync initializations @@ -49,21 +49,23 @@ class NormalChat: self.start_time = time.time() self.last_speak_time = 0 self._chat_task: Optional[asyncio.Task] = None - self._initialized = False # Track initialization status - - # logger.info(f"[{self.stream_name}] NormalChat 实例 __init__ 完成 (同步部分)。") + self._initialized = False # Track initialization status + + # logger.info(f"[{self.stream_name}] NormalChat 实例 __init__ 完成 (同步部分)。") # Avoid logging here as stream_name might not be final async def initialize(self): """异步初始化,获取聊天类型和目标信息。""" if self._initialized: return - + # --- Use utility function to determine chat type and fetch info --- self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.stream_id) # Update stream_name again after potential async call in util func - self.stream_name = chat_manager.get_stream_name(self.stream_id) or self.stream_id - logger.debug(f"[{self.stream_name}] NormalChat initialized: is_group={self.is_group_chat}, target_info={self.chat_target_info}") + self.stream_name = chat_manager.get_stream_name(self.stream_id) or self.stream_id + logger.debug( + f"[{self.stream_name}] NormalChat initialized: is_group={self.is_group_chat}, target_info={self.chat_target_info}" + ) # --- End using utility function --- self._initialized = True logger.info(f"[{self.stream_name}] NormalChat 实例 initialize 完成 (异步部分)。") @@ -437,8 +439,8 @@ class NormalChat: async def start_chat(self): """先进行异步初始化,然后启动聊天任务。""" if not self._initialized: - await self.initialize() # Ensure initialized before starting tasks - + await self.initialize() # Ensure initialized before starting tasks + if self._chat_task is None or self._chat_task.done(): logger.info(f"[{self.stream_name}] 开始后台处理初始兴趣消息和轮询任务...") # Process initial messages first From f4616afb3a34d4db5ab5314bb24748d537859f53 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 1 May 2025 23:21:49 +0800 Subject: [PATCH 32/67] =?UTF-8?q?feat=EF=BC=9A=E6=B7=BB=E5=8A=A0=E5=8F=96?= =?UTF-8?q?=E5=90=8D=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tool_can_use/rename_person_tool.py | 112 ++++++++++++++++++ src/plugins/person_info/person_info.py | 42 ++++++- 2 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 src/do_tool/tool_can_use/rename_person_tool.py diff --git a/src/do_tool/tool_can_use/rename_person_tool.py b/src/do_tool/tool_can_use/rename_person_tool.py new file mode 100644 index 00000000..875e4ddc --- /dev/null +++ b/src/do_tool/tool_can_use/rename_person_tool.py @@ -0,0 +1,112 @@ +from src.do_tool.tool_can_use.base_tool import BaseTool, register_tool +from src.plugins.person_info.person_info import person_info_manager +from src.common.logger_manager import get_logger +import json + +logger = get_logger("rename_person_tool") + +class RenamePersonTool(BaseTool): + name = "rename_person" + description = "这个工具可以改变用户的昵称。你可以选择改变对他人的称呼。" + parameters = { + "type": "object", + "properties": { + "person_name": { + "type": "string", + "description": "需要重新取名的用户的当前昵称" + }, + "message_content": { + "type": "string", + "description": "可选的。当前的聊天内容或特定要求,用于提供取名建议的上下文。" + } + }, + "required": ["person_name"] + } + + async def execute(self, function_args: dict, message_txt=""): + """ + 执行取名工具逻辑 + + Args: + function_args (dict): 包含 'person_name' 和可选 'message_content' 的字典 + message_txt (str): 原始消息文本 (这里未使用,因为 message_content 更明确) + + Returns: + dict: 包含执行结果的字典 + """ + person_name_to_find = function_args.get("person_name") + request_context = function_args.get("message_content", "") # 如果没有提供,则为空字符串 + + if not person_name_to_find: + return { + "name": self.name, + "content": "错误:必须提供需要重命名的用户昵称 (person_name)。" + } + + try: + # 1. 根据昵称查找用户信息 + logger.debug(f"尝试根据昵称 '{person_name_to_find}' 查找用户...") + person_info = await person_info_manager.get_person_info_by_name(person_name_to_find) + + if not person_info: + logger.info(f"未找到昵称为 '{person_name_to_find}' 的用户。") + return { + "name": self.name, + "content": f"找不到昵称为 '{person_name_to_find}' 的用户。请确保输入的是我之前为该用户取的昵称。" + } + + person_id = person_info.get("person_id") + user_nickname = person_info.get("nickname") # 这是用户原始昵称 + user_cardname = person_info.get("user_cardname") + user_avatar = person_info.get("user_avatar") + + if not person_id: + logger.error(f"找到了用户 '{person_name_to_find}' 但无法获取 person_id") + return { + "name": self.name, + "content": f"找到了用户 '{person_name_to_find}' 但获取内部ID时出错。" + } + + # 2. 调用 qv_person_name 进行取名 + logger.debug(f"为用户 {person_id} (原昵称: {person_name_to_find}) 调用 qv_person_name,请求上下文: '{request_context}'") + result = await person_info_manager.qv_person_name( + person_id=person_id, + user_nickname=user_nickname, + user_cardname=user_cardname, + user_avatar=user_avatar, + request=request_context + ) + + # 3. 处理结果 + if result and result.get("nickname"): + new_name = result["nickname"] + reason = result.get("reason", "未提供理由") + logger.info(f"成功为用户 {person_id} 取了新昵称: {new_name}") + return { + "name": self.name, + "content": f"好的,我已经给 '{person_name_to_find}' 取了新昵称:'{new_name}'。因为:{reason}" + } + else: + logger.warning(f"为用户 {person_id} 调用 qv_person_name 后未能成功获取新昵称。") + # 尝试从内存中获取可能已经更新的名字 + current_name = await person_info_manager.get_value(person_id, "person_name") + if current_name and current_name != person_name_to_find: + return { + "name": self.name, + "content": f"尝试取新昵称时遇到一点小问题,但我已经将 '{person_name_to_find}' 的昵称更新为 '{current_name}' 了。" + } + else: + return { + "name": self.name, + "content": f"尝试为 '{person_name_to_find}' 取新昵称时遇到了问题,未能成功生成。可能需要稍后再试。" + } + + except Exception as e: + logger.error(f"执行 rename_person 工具时出错: {e}", exc_info=True) + return { + "name": self.name, + "content": f"执行重命名操作时遇到内部错误: {e}" + } + +# 注册工具 +register_tool(RenamePersonTool) \ No newline at end of file diff --git a/src/plugins/person_info/person_info.py b/src/plugins/person_info/person_info.py index a71f95f8..e95fffdb 100644 --- a/src/plugins/person_info/person_info.py +++ b/src/plugins/person_info/person_info.py @@ -51,6 +51,8 @@ person_info_default = { "konw_time": 0, "msg_interval": 2000, "msg_interval_list": [], + "user_cardname": None, # 添加群名片 + "user_avatar": None, # 添加头像信息(例如URL或标识符) } # 个人信息的各项与默认值在此定义,以下处理会自动创建/补全每一项 @@ -186,7 +188,7 @@ class PersonInfoManager: logger.warning(f"无法从文本中提取有效的JSON字典: {text}") return {"nickname": "", "reason": ""} - async def qv_person_name(self, person_id: str, user_nickname: str, user_cardname: str, user_avatar: str): + async def qv_person_name(self, person_id: str, user_nickname: str, user_cardname: str, user_avatar: str, request: str = ""): """给某个用户取名""" if not person_id: logger.debug("取名失败:person_id不能为空") @@ -211,6 +213,8 @@ class PersonInfoManager: if old_name: qv_name_prompt += f"你之前叫他{old_name},是因为{old_reason}," + qv_name_prompt += f"\n其他取名的要求是:{request}" + qv_name_prompt += "\n请根据以上用户信息,想想你叫他什么比较好,请最好使用用户的qq昵称,可以稍作修改" if existing_names: qv_name_prompt += f"\n请注意,以下名称已被使用,不要使用以下昵称:{existing_names}。\n" @@ -511,5 +515,41 @@ class PersonInfoManager: return person_id + async def get_person_info_by_name(self, person_name: str) -> dict | None: + """根据 person_name 查找用户并返回基本信息 (如果找到)""" + if not person_name: + logger.debug("get_person_info_by_name 获取失败:person_name 不能为空") + return None + + # 优先从内存缓存查找 person_id + found_person_id = None + for pid, name in self.person_name_list.items(): + if name == person_name: + found_person_id = pid + break # 找到第一个匹配就停止 + + if not found_person_id: + # 如果内存没有,尝试数据库查询(可能内存未及时更新或启动时未加载) + document = db.person_info.find_one({"person_name": person_name}) + if document: + found_person_id = document.get("person_id") + else: + logger.debug(f"数据库中也未找到名为 '{person_name}' 的用户") + return None # 数据库也找不到 + + # 根据找到的 person_id 获取所需信息 + if found_person_id: + required_fields = ["person_id", "platform", "user_id", "nickname", "user_cardname", "user_avatar"] + person_data = await self.get_values(found_person_id, required_fields) + if person_data: # 确保 get_values 成功返回 + return person_data + else: + logger.warning(f"找到了 person_id '{found_person_id}' 但获取详细信息失败") + return None + else: + # 这理论上不应该发生,因为上面已经处理了找不到的情况 + logger.error(f"逻辑错误:未能为 '{person_name}' 确定 person_id") + return None + person_info_manager = PersonInfoManager() From f30d472c0bdc8dffcb8b11a0450277387739ad2a Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 2 May 2025 00:58:55 +0800 Subject: [PATCH 33/67] =?UTF-8?q?feat=EF=BC=9A=E4=BC=98=E5=8C=96=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E8=B0=83=E7=94=A8=E7=9A=84=E5=BE=97=E5=88=B0=E7=9A=84?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E8=B0=83=E7=94=A8=E5=92=8C=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tool_can_use/compare_numbers_tool.py | 6 +- src/do_tool/tool_can_use/get_knowledge.py | 8 +- src/do_tool/tool_can_use/get_memory.py | 7 +- src/do_tool/tool_can_use/get_time_date.py | 4 +- .../tool_can_use/lpmm_get_knowledge.py | 28 +++++- .../tool_can_use/rename_person_tool.py | 18 ++-- src/heart_flow/observation.py | 5 +- src/heart_flow/sub_mind.py | 98 +++++++++++++++---- src/heart_flow/subheartflow_manager.py | 2 + src/heart_flow/utils_chat.py | 2 +- src/plugins/chat/message.py | 15 ++- src/plugins/chat/utils.py | 47 --------- src/plugins/heartFC_chat/heartFC_chat.py | 7 +- src/plugins/heartFC_chat/normal_chat.py | 20 ++-- 14 files changed, 154 insertions(+), 113 deletions(-) diff --git a/src/do_tool/tool_can_use/compare_numbers_tool.py b/src/do_tool/tool_can_use/compare_numbers_tool.py index ef037de9..2bb292a1 100644 --- a/src/do_tool/tool_can_use/compare_numbers_tool.py +++ b/src/do_tool/tool_can_use/compare_numbers_tool.py @@ -9,7 +9,7 @@ class CompareNumbersTool(BaseTool): """比较两个数大小的工具""" name = "compare_numbers" - description = "比较两个数的大小,返回较大的数" + description = "使用工具 比较两个数的大小,返回较大的数" parameters = { "type": "object", "properties": { @@ -39,10 +39,10 @@ class CompareNumbersTool(BaseTool): else: result = f"{num1} 等于 {num2}" - return {"name": self.name, "content": result} + return {"type": "comparison_result", "id": f"{num1}_vs_{num2}", "content": result} except Exception as e: logger.error(f"比较数字失败: {str(e)}") - return {"name": self.name, "content": f"比较数字失败: {str(e)}"} + return {"type": "info", "id": f"{num1}_vs_{num2}", "content": f"比较数字失败,炸了: {str(e)}"} # 注册工具 diff --git a/src/do_tool/tool_can_use/get_knowledge.py b/src/do_tool/tool_can_use/get_knowledge.py index 20a92264..90a44655 100644 --- a/src/do_tool/tool_can_use/get_knowledge.py +++ b/src/do_tool/tool_can_use/get_knowledge.py @@ -11,7 +11,7 @@ class SearchKnowledgeTool(BaseTool): """从知识库中搜索相关信息的工具""" name = "search_knowledge" - description = "从知识库中搜索相关信息" + description = "使用工具从知识库中搜索相关信息" parameters = { "type": "object", "properties": { @@ -42,11 +42,11 @@ class SearchKnowledgeTool(BaseTool): content = f"你知道这些知识: {knowledge_info}" else: content = f"你不太了解有关{query}的知识" - return {"name": "search_knowledge", "content": content} - return {"name": "search_knowledge", "content": f"无法获取关于'{query}'的嵌入向量"} + return {"type": "knowledge", "id": query, "content": content} + return {"type": "info", "id": query, "content": f"无法获取关于'{query}'的嵌入向量,你知识库炸了"} except Exception as e: logger.error(f"知识库搜索工具执行失败: {str(e)}") - return {"name": "search_knowledge", "content": f"知识库搜索失败: {str(e)}"} + return {"type": "info", "id": query, "content": f"知识库搜索失败,炸了: {str(e)}"} @staticmethod def get_info_from_db( diff --git a/src/do_tool/tool_can_use/get_memory.py b/src/do_tool/tool_can_use/get_memory.py index c1bcc927..481942da 100644 --- a/src/do_tool/tool_can_use/get_memory.py +++ b/src/do_tool/tool_can_use/get_memory.py @@ -10,7 +10,7 @@ class GetMemoryTool(BaseTool): """从记忆系统中获取相关记忆的工具""" name = "get_memory" - description = "从记忆系统中获取相关记忆" + description = "使用工具从记忆系统中获取相关记忆" parameters = { "type": "object", "properties": { @@ -53,10 +53,11 @@ class GetMemoryTool(BaseTool): else: content = f"{topic}的记忆,你记不太清" - return {"name": "get_memory", "content": content} + return {"type": "memory", "id": topic_list, "content": content} except Exception as e: logger.error(f"记忆获取工具执行失败: {str(e)}") - return {"name": "get_memory", "content": f"记忆获取失败: {str(e)}"} + # 在失败时也保持格式一致,但id可能不适用或设为None/Error + return {"type": "memory_error", "id": topic_list, "content": f"记忆获取失败: {str(e)}"} # 注册工具 diff --git a/src/do_tool/tool_can_use/get_time_date.py b/src/do_tool/tool_can_use/get_time_date.py index bd3b1a9c..1cb23fdb 100644 --- a/src/do_tool/tool_can_use/get_time_date.py +++ b/src/do_tool/tool_can_use/get_time_date.py @@ -2,6 +2,7 @@ from src.do_tool.tool_can_use.base_tool import BaseTool from src.common.logger_manager import get_logger from typing import Dict, Any from datetime import datetime +import time logger = get_logger("get_time_date") @@ -32,6 +33,7 @@ class GetCurrentDateTimeTool(BaseTool): current_weekday = datetime.now().strftime("%A") return { - "name": "get_current_date_time", + "type": "time_info", + "id": f"time_info_{time.time()}", "content": f"当前时间: {current_time}, 日期: {current_date}, 年份: {current_year}, 星期: {current_weekday}", } diff --git a/src/do_tool/tool_can_use/lpmm_get_knowledge.py b/src/do_tool/tool_can_use/lpmm_get_knowledge.py index 8754e603..5eec260d 100644 --- a/src/do_tool/tool_can_use/lpmm_get_knowledge.py +++ b/src/do_tool/tool_can_use/lpmm_get_knowledge.py @@ -46,11 +46,14 @@ class SearchKnowledgeFromLPMMTool(BaseTool): content = f"你知道这些知识: {knowledge_info}" else: content = f"你不太了解有关{query}的知识" - return {"name": "search_knowledge", "content": content} - return {"name": "search_knowledge", "content": f"无法获取关于'{query}'的嵌入向量"} + return {"type": "lpmm_knowledge", "id": query, "content": content} + # 如果获取嵌入失败 + return {"type": "info", "id": query, "content": f"无法获取关于'{query}'的嵌入向量,你lpmm知识库炸了"} except Exception as e: logger.error(f"知识库搜索工具执行失败: {str(e)}") - return {"name": "search_knowledge", "content": f"知识库搜索失败: {str(e)}"} + # 在其他异常情况下,确保 id 仍然是 query (如果它被定义了) + query_id = query if 'query' in locals() else 'unknown_query' + return {"type": "info", "id": query_id, "content": f"lpmm知识库搜索失败,炸了: {str(e)}"} # def get_info_from_db( # self, query_embedding: list, limit: int = 1, threshold: float = 0.5, return_raw: bool = False @@ -133,6 +136,25 @@ class SearchKnowledgeFromLPMMTool(BaseTool): # # 返回所有找到的内容,用换行分隔 # return "\n".join(str(result["content"]) for result in results) + def _format_results(self, results: list) -> str: + """格式化结果""" + if not results: + return "未找到相关知识。" + + formatted_string = "我找到了一些相关知识:\n" + for i, result in enumerate(results): + chunk_id = result.get("chunk_id") + text = result.get("text", "") + source = result.get("source", "未知来源") + source_type = result.get("source_type", "未知类型") + similarity = result.get("similarity", 0.0) + + formatted_string += f"{i + 1}. (相似度: {similarity:.2f}) 类型: {source_type}, 来源: {source} \n内容片段: {text}\n\n" + # 暂时去掉chunk_id + # formatted_string += f"{i + 1}. (相似度: {similarity:.2f}) 类型: {source_type}, 来源: {source}, Chunk ID: {chunk_id} \n内容片段: {text}\n\n" + + return formatted_string + # 注册工具 # register_tool(SearchKnowledgeTool) diff --git a/src/do_tool/tool_can_use/rename_person_tool.py b/src/do_tool/tool_can_use/rename_person_tool.py index 875e4ddc..9e796931 100644 --- a/src/do_tool/tool_can_use/rename_person_tool.py +++ b/src/do_tool/tool_can_use/rename_person_tool.py @@ -1,7 +1,9 @@ from src.do_tool.tool_can_use.base_tool import BaseTool, register_tool from src.plugins.person_info.person_info import person_info_manager from src.common.logger_manager import get_logger +from src.plugins.person_info.relationship_manager import relationship_manager import json +import time logger = get_logger("rename_person_tool") @@ -82,10 +84,10 @@ class RenamePersonTool(BaseTool): new_name = result["nickname"] reason = result.get("reason", "未提供理由") logger.info(f"成功为用户 {person_id} 取了新昵称: {new_name}") - return { - "name": self.name, - "content": f"好的,我已经给 '{person_name_to_find}' 取了新昵称:'{new_name}'。因为:{reason}" - } + + content = f"已成功将用户 {person_name_to_find} 的备注名更新为 {new_name}" + logger.info(content) + return {"type": "info", "id": f"rename_success_{time.time()}", "content": content} else: logger.warning(f"为用户 {person_id} 调用 qv_person_name 后未能成功获取新昵称。") # 尝试从内存中获取可能已经更新的名字 @@ -102,11 +104,9 @@ class RenamePersonTool(BaseTool): } except Exception as e: - logger.error(f"执行 rename_person 工具时出错: {e}", exc_info=True) - return { - "name": self.name, - "content": f"执行重命名操作时遇到内部错误: {e}" - } + error_msg = f"重命名失败: {str(e)}" + logger.error(error_msg, exc_info=True) + return {"type": "info_error", "id": f"rename_error_{time.time()}", "content": error_msg} # 注册工具 register_tool(RenamePersonTool) \ No newline at end of file diff --git a/src/heart_flow/observation.py b/src/heart_flow/observation.py index 0d51c938..0c63e751 100644 --- a/src/heart_flow/observation.py +++ b/src/heart_flow/observation.py @@ -79,9 +79,8 @@ class ChattingObservation(Observation): async def initialize(self): # --- Use utility function to determine chat type and fetch info --- self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.chat_id) - logger.debug( - f"ChattingObservation {self.chat_id} initialized: is_group={self.is_group_chat}, target_info={self.chat_target_info}" - ) + # logger.debug(f"is_group_chat: {self.is_group_chat}") + # logger.debug(f"chat_target_info: {self.chat_target_info}") # --- End using utility function --- # Fetch initial messages (existing logic) diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index 4ce2fe41..0dfc836c 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -1,4 +1,4 @@ -from .observation import Observation +from .observation import Observation, ChattingObservation from src.plugins.models.utils_model import LLMRequest from src.config.config import global_config import time @@ -114,7 +114,7 @@ def calculate_replacement_probability(similarity: float) -> float: class SubMind: - def __init__(self, subheartflow_id: str, chat_state: ChatStateInfo, observations: Observation): + def __init__(self, subheartflow_id: str, chat_state: ChatStateInfo, observations: ChattingObservation): self.last_active_time = None self.subheartflow_id = subheartflow_id @@ -130,10 +130,41 @@ class SubMind: self.current_mind = "" self.past_mind = [] - self.structured_info = {} + self.structured_info = [] + self.structured_info_str = "" name = chat_manager.get_stream_name(self.subheartflow_id) self.log_prefix = f"[{name}] " + self._update_structured_info_str() + + def _update_structured_info_str(self): + """根据 structured_info 更新 structured_info_str""" + if not self.structured_info: + self.structured_info_str = "" + return + + lines = ["【信息】"] + for item in self.structured_info: + # 简化展示,突出内容和类型,包含TTL供调试 + type_str = item.get('type', '未知类型') + content_str = item.get('content', '') + ttl = item.get('ttl', '?') + + if type_str == "info": + lines.append(f"刚刚: {content_str}") + elif type_str == "memory": + lines.append(f"{content_str}") + elif type_str == "comparison_result": + lines.append(f"数字大小比较结果: {content_str}") + elif type_str == "time_info": + lines.append(f"{content_str}") + elif type_str == "lpmm_knowledge": + lines.append(f"你知道:{content_str}") + else: + lines.append(f"{type_str}的信息: {content_str}") + + self.structured_info_str = "\n".join(lines) + logger.debug(f"{self.log_prefix} 更新 structured_info_str: \n{self.structured_info_str}") async def do_thinking_before_reply(self, history_cycle: list[CycleInfo] = None): """ @@ -145,19 +176,36 @@ class SubMind: # 更新活跃时间 self.last_active_time = time.time() + # ---------- 0. 更新和清理 structured_info ---------- + if self.structured_info: + logger.debug(f"{self.log_prefix} 更新前的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}") + updated_info = [] + for item in self.structured_info: + item['ttl'] -= 1 + if item['ttl'] > 0: + updated_info.append(item) + else: + logger.debug(f"{self.log_prefix} 移除过期的 structured_info 项: {item['id']}") + self.structured_info = updated_info + logger.debug(f"{self.log_prefix} 更新后的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}") + self._update_structured_info_str() + logger.debug(f"{self.log_prefix} 当前完整的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}") + # ---------- 1. 准备基础数据 ---------- # 获取现有想法和情绪状态 previous_mind = self.current_mind if self.current_mind else "" mood_info = self.chat_state.mood # 获取观察对象 - observation = self.observations[0] if self.observations else None + observation: ChattingObservation = self.observations[0] if self.observations else None if not observation or not hasattr(observation, "is_group_chat"): # Ensure it's ChattingObservation or similar logger.error(f"{self.log_prefix} 无法获取有效的观察对象或缺少聊天类型信息") self.update_current_mind("(观察出错了...)") return self.current_mind, self.past_mind - + is_group_chat = observation.is_group_chat + # logger.debug(f"is_group_chat: {is_group_chat}") + chat_target_info = observation.chat_target_info chat_target_name = "对方" # Default for private if not is_group_chat and chat_target_info: @@ -277,10 +325,11 @@ class SubMind: # ---------- 4. 构建最终提示词 ---------- # --- Choose template based on chat type --- + logger.debug(f"is_group_chat: {is_group_chat}") if is_group_chat: template_name = "sub_heartflow_prompt_before" prompt = (await global_prompt_manager.get_prompt_async(template_name)).format( - extra_info="", + extra_info=self.structured_info_str, prompt_personality=prompt_personality, relation_prompt=relation_prompt, bot_name=individuality.name, @@ -295,7 +344,7 @@ class SubMind: else: # Private chat template_name = "sub_heartflow_prompt_private_before" prompt = (await global_prompt_manager.get_prompt_async(template_name)).format( - extra_info="", + extra_info=self.structured_info_str, prompt_personality=prompt_personality, relation_prompt=relation_prompt, # Might need adjustment for private context bot_name=individuality.name, @@ -447,7 +496,7 @@ class SubMind: tool_instance: 工具使用器实例 """ tool_results = [] - structured_info = {} # 动态生成键 + new_structured_items = [] # 收集新产生的结构化信息 # 执行所有工具调用 for tool_call in tool_calls: @@ -455,23 +504,34 @@ class SubMind: result = await tool_instance._execute_tool_call(tool_call) if result: tool_results.append(result) + # 创建新的结构化信息项 + new_item = { + "type": result.get("type", "unknown_type"), # 使用 'type' 键 + "id": result.get("id", f"fallback_id_{time.time()}"), # 使用 'id' 键 + "content": result.get("content", ""), # 'content' 键保持不变 + "ttl": 3 + } + new_structured_items.append(new_item) - # 使用工具名称作为键 - 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}") + logger.error(traceback.format_exc()) # 添加 traceback 记录 - # 如果有工具结果,记录并更新结构化信息 - if structured_info: - logger.debug(f"工具调用收集到结构化信息: {safe_json_dumps(structured_info, ensure_ascii=False)}") - self.structured_info = structured_info + # 如果有新的工具结果,记录并更新结构化信息 + if new_structured_items: + self.structured_info.extend(new_structured_items) # 添加到现有列表 + logger.debug(f"工具调用收集到新的结构化信息: {safe_json_dumps(new_structured_items, ensure_ascii=False)}") + # logger.debug(f"当前完整的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}") # 可以取消注释以查看完整列表 + self._update_structured_info_str() # 添加新信息后,更新字符串表示 def update_current_mind(self, response): - self.past_mind.append(self.current_mind) + if self.current_mind: # 只有当 current_mind 非空时才添加到 past_mind + self.past_mind.append(self.current_mind) + # 可以考虑限制 past_mind 的大小,例如: + # max_past_mind_size = 10 + # if len(self.past_mind) > max_past_mind_size: + # self.past_mind.pop(0) # 移除最旧的 + self.current_mind = response diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index 04153abf..fa8aaa96 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -127,6 +127,8 @@ class SubHeartflowManager: # 添加聊天观察者 observation = ChattingObservation(chat_id=subheartflow_id) + await observation.initialize() + new_subflow.add_observation(observation) # 注册子心流 diff --git a/src/heart_flow/utils_chat.py b/src/heart_flow/utils_chat.py index 3e082b6d..c3f81a14 100644 --- a/src/heart_flow/utils_chat.py +++ b/src/heart_flow/utils_chat.py @@ -18,7 +18,7 @@ async def get_chat_type_and_target_info(chat_id: str) -> Tuple[bool, Optional[Di Tuple[bool, Optional[Dict]]: - bool: 是否为群聊 (True 是群聊, False 是私聊或未知) - Optional[Dict]: 如果是私聊,包含对方信息的字典;否则为 None。 - 字典包含: platform, user_id, user_nickname, person_id, person_name + 字典包含: platform, user_id, user_nickname, person_id, person_name """ is_group_chat = False # Default to private/unknown chat_target_info = None diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py index e753a999..a29d0ab2 100644 --- a/src/plugins/chat/message.py +++ b/src/plugins/chat/message.py @@ -31,19 +31,21 @@ class Message(MessageBase): def __init__( self, message_id: str, - timestamp: float, chat_stream: ChatStream, user_info: UserInfo, message_segment: Optional[Seg] = None, + timestamp: Optional[float] = None, reply: Optional["MessageRecv"] = None, detailed_plain_text: str = "", processed_plain_text: str = "", ): + # 使用传入的时间戳或当前时间 + current_timestamp = timestamp if timestamp is not None else round(time.time(), 3) # 构造基础消息信息 message_info = BaseMessageInfo( platform=chat_stream.platform, message_id=message_id, - time=timestamp, + time=current_timestamp, group_info=chat_stream.group_info, user_info=user_info, ) @@ -164,11 +166,12 @@ class MessageProcessBase(Message): message_segment: Optional[Seg] = None, reply: Optional["MessageRecv"] = None, thinking_start_time: float = 0, + timestamp: Optional[float] = None, ): - # 调用父类初始化 + # 调用父类初始化,传递时间戳 super().__init__( message_id=message_id, - timestamp=round(time.time(), 3), # 保留3位小数 + timestamp=timestamp, chat_stream=chat_stream, user_info=bot_user_info, message_segment=message_segment, @@ -238,8 +241,9 @@ class MessageThinking(MessageProcessBase): bot_user_info: UserInfo, reply: Optional["MessageRecv"] = None, thinking_start_time: float = 0, + timestamp: Optional[float] = None, ): - # 调用父类初始化 + # 调用父类初始化,传递时间戳 super().__init__( message_id=message_id, chat_stream=chat_stream, @@ -247,6 +251,7 @@ class MessageThinking(MessageProcessBase): message_segment=None, # 思考状态不需要消息段 reply=reply, thinking_start_time=thinking_start_time, + timestamp=timestamp, ) # 思考状态特有属性 diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index cdaa2194..9b71ecc5 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -107,53 +107,6 @@ async def get_embedding(text, request_type="embedding"): return embedding -async def get_recent_group_messages(chat_id: str, limit: int = 12) -> list: - """从数据库获取群组最近的消息记录 - - Args: - chat_id: 群组ID - limit: 获取消息数量,默认12条 - - Returns: - list: Message对象列表,按时间正序排列 - """ - - # 从数据库获取最近消息 - recent_messages = list( - db.messages.find( - {"chat_id": chat_id}, - ) - .sort("time", -1) - .limit(limit) - ) - - if not recent_messages: - return [] - - # 转换为 Message对象列表 - message_objects = [] - for msg_data in recent_messages: - try: - chat_info = msg_data.get("chat_info", {}) - chat_stream = ChatStream.from_dict(chat_info) - user_info = msg_data.get("user_info", {}) - user_info = UserInfo.from_dict(user_info) - msg = Message( - message_id=msg_data["message_id"], - chat_stream=chat_stream, - timestamp=msg_data["time"], - user_info=user_info, - processed_plain_text=msg_data.get("processed_text", ""), - detailed_plain_text=msg_data.get("detailed_plain_text", ""), - ) - message_objects.append(msg) - except KeyError: - logger.warning("数据库中存在无效的消息") - continue - - # 按时间正序排列 - message_objects.reverse() - return message_objects def get_recent_group_detailed_plain_text(chat_stream_id: str, limit: int = 12, combine=False): diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index dd078f7c..f203926f 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -264,9 +264,6 @@ class HeartFChatting: logger.error(f"[HFC:{self.stream_id}] 获取ChatStream时出错 in _initialize: {e}") return False - logger.debug( - f"{self.log_prefix} HeartFChatting initialized: is_group={self.is_group_chat}, target_info={self.chat_target_info}" - ) # --- End using utility function --- self._initialized = True @@ -861,7 +858,7 @@ class HeartFChatting: cycle_history=self._cycle_history, # <-- Pass HFC state observed_messages_str=observed_messages_str, # <-- Pass local variable current_mind=current_mind, # <-- Pass argument - structured_info=self.sub_mind.structured_info, # <-- Pass SubMind info + structured_info=self.sub_mind.structured_info_str, # <-- Pass SubMind info current_available_actions=current_available_actions, # <-- Pass determined actions ) @@ -1301,7 +1298,7 @@ class HeartFChatting: # Focus specific args: reason=reason, current_mind_info=self.sub_mind.current_mind, - structured_info=self.sub_mind.structured_info, + structured_info=self.sub_mind.structured_info_str, sender_name=sender_name_for_prompt, # Pass determined name # Normal specific args (not used in focus mode): # message_txt="", diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py index 6b8fb9ce..dd89782f 100644 --- a/src/plugins/heartFC_chat/normal_chat.py +++ b/src/plugins/heartFC_chat/normal_chat.py @@ -63,15 +63,12 @@ class NormalChat: self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.stream_id) # Update stream_name again after potential async call in util func self.stream_name = chat_manager.get_stream_name(self.stream_id) or self.stream_id - logger.debug( - f"[{self.stream_name}] NormalChat initialized: is_group={self.is_group_chat}, target_info={self.chat_target_info}" - ) # --- End using utility function --- self._initialized = True logger.info(f"[{self.stream_name}] NormalChat 实例 initialize 完成 (异步部分)。") # 改为实例方法 - async def _create_thinking_message(self, message: MessageRecv) -> str: + async def _create_thinking_message(self, message: MessageRecv, timestamp: Optional[float] = None) -> str: """创建思考消息""" messageinfo = message.message_info @@ -85,15 +82,15 @@ class NormalChat: thinking_id = "mt" + str(thinking_time_point) thinking_message = MessageThinking( message_id=thinking_id, - chat_stream=self.chat_stream, # 使用 self.chat_stream + chat_stream=self.chat_stream, bot_user_info=bot_user_info, reply=message, thinking_start_time=thinking_time_point, + timestamp=timestamp if timestamp is not None else None ) await message_manager.add_message(thinking_message) return thinking_id - # 改为实例方法 async def _add_messages_to_manager( self, message: MessageRecv, response_set: List[str], thinking_id @@ -209,7 +206,7 @@ class NormalChat: try: # 处理消息 await self.normal_response( - message=message, is_mentioned=is_mentioned, interested_rate=interest_value + message=message, is_mentioned=is_mentioned, interested_rate=interest_value, rewind_response = False ) except Exception as e: logger.error(f"[{self.stream_name}] 处理兴趣消息{msg_id}时出错: {e}\n{traceback.format_exc()}") @@ -217,7 +214,7 @@ class NormalChat: self.interest_dict.pop(msg_id, None) # 改为实例方法, 移除 chat 参数 - async def normal_response(self, message: MessageRecv, is_mentioned: bool, interested_rate: float) -> None: + async def normal_response(self, message: MessageRecv, is_mentioned: bool, interested_rate: float, rewind_response: bool = False) -> None: # 检查收到的消息是否属于当前实例处理的 chat stream if message.chat_stream.stream_id != self.stream_id: logger.error( @@ -264,7 +261,10 @@ class NormalChat: await willing_manager.before_generate_reply_handle(message.message_info.message_id) with Timer("创建思考消息", timing_results): - thinking_id = await self._create_thinking_message(message) + if rewind_response: + thinking_id = await self._create_thinking_message(message, message.message_info.time) + else: + thinking_id = await self._create_thinking_message(message) logger.debug(f"[{self.stream_name}] 创建捕捉器,thinking_id:{thinking_id}") @@ -393,7 +393,7 @@ class NormalChat: try: logger.info(f"[{self.stream_name}] 处理初始高兴趣消息 {msg_id} (兴趣值: {interest_value:.2f})") - await self.normal_response(message=message, is_mentioned=is_mentioned, interested_rate=interest_value) + await self.normal_response(message=message, is_mentioned=is_mentioned, interested_rate=interest_value, rewind_response = True) processed_count += 1 except Exception as e: logger.error(f"[{self.stream_name}] 处理初始兴趣消息 {msg_id} 时出错: {e}\\n{traceback.format_exc()}") From c6d831f56f03034c77f6d2b9ef7126d5ac197b49 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 2 May 2025 00:59:04 +0800 Subject: [PATCH 34/67] =?UTF-8?q?feat=EF=BC=9A=E7=A7=81=E8=81=8A=E7=8E=B0?= =?UTF-8?q?=E6=9C=89=E7=99=BD=E5=90=8D=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/config.py | 2 ++ src/plugins/chat/bot.py | 8 ++++++++ template/bot_config_template.toml | 3 ++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/config/config.py b/src/config/config.py index 9b4386e7..f5be7ec6 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -268,6 +268,7 @@ class BotConfig: # experimental enable_friend_chat: bool = False # 是否启用好友聊天 # enable_think_flow: bool = False # 是否启用思考流程 + talk_allowed_private = set() enable_pfc_chatting: bool = False # 是否启用PFC聊天 # 模型配置 @@ -651,6 +652,7 @@ class BotConfig: experimental_config = parent["experimental"] config.enable_friend_chat = experimental_config.get("enable_friend_chat", config.enable_friend_chat) # config.enable_think_flow = experimental_config.get("enable_think_flow", config.enable_think_flow) + config.talk_allowed_private = set(str(user) for user in experimental_config.get("talk_allowed_private", [])) if config.INNER_VERSION in SpecifierSet(">=1.1.0"): config.enable_pfc_chatting = experimental_config.get("pfc_chatting", config.enable_pfc_chatting) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 89e171ba..dd65ca1c 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -82,6 +82,14 @@ class ChatBot: if userinfo.user_id in global_config.ban_user_id: logger.debug(f"用户{userinfo.user_id}被禁止回复") return + + if groupinfo is None: + logger.trace("检测到私聊消息,检查") + # 好友黑名单拦截 + if userinfo.user_id not in global_config.talk_allowed_private: + logger.debug(f"用户{userinfo.user_id}没有私聊权限") + return + # 群聊黑名单拦截 if groupinfo is not None and groupinfo.group_id not in global_config.talk_allowed_groups: diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index c924d35a..5f215009 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "1.6.0" +version = "1.6.1" #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #如果你想要修改配置文件,请在修改后将version的值进行变更 @@ -186,6 +186,7 @@ enable = true [experimental] #实验性功能 enable_friend_chat = false # 是否启用好友聊天 +talk_allowed_private = [] # 可以回复消息的QQ号 pfc_chatting = false # 是否启用PFC聊天,该功能仅作用于私聊,与回复模式独立 #下面的模型若使用硅基流动则不需要更改,使用ds官方则改成.env自定义的宏,使用自定义模型则选择定位相似的模型自己填写 From 1ca736cc32e613989055e6ce55ba7e1093e0402c Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 2 May 2025 01:04:39 +0800 Subject: [PATCH 35/67] =?UTF-8?q?feat=EF=BC=9A=E4=BF=AE=E5=A4=8D=E8=BF=9B?= =?UTF-8?q?=E5=85=A5=E6=99=AE=E9=80=9A=E8=81=8A=E5=A4=A9=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E7=9A=84=E5=96=B7=E5=B0=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/heartFC_chat/normal_chat.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py index dd89782f..5b5e8e88 100644 --- a/src/plugins/heartFC_chat/normal_chat.py +++ b/src/plugins/heartFC_chat/normal_chat.py @@ -398,6 +398,11 @@ class NormalChat: except Exception as e: logger.error(f"[{self.stream_name}] 处理初始兴趣消息 {msg_id} 时出错: {e}\\n{traceback.format_exc()}") + # --- 新增:处理完后清空整个字典 --- + logger.info(f"[{self.stream_name}] 处理了 {processed_count} 条初始高兴趣消息。现在清空所有剩余的初始兴趣消息...") + self.interest_dict.clear() + # --- 新增结束 --- + logger.info( f"[{self.stream_name}] 初始高兴趣消息处理完毕,共处理 {processed_count} 条。剩余 {len(self.interest_dict)} 条待轮询。" ) From 1c956d414983ceab0460e81a836f7a96badc7cb0 Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Fri, 2 May 2025 01:16:23 +0800 Subject: [PATCH 36/67] =?UTF-8?q?=E5=90=8C=E6=AD=A5hfc=E7=9A=84=E8=AE=B0?= =?UTF-8?q?=E5=BF=86=E7=9F=A5=E8=AF=86=E6=A3=80=E7=B4=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/action_planner.py | 234 +++++++++++++++++++++++-- src/plugins/PFC/reply_generator.py | 265 ++++++++++++++++++++++++++++- 2 files changed, 483 insertions(+), 16 deletions(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 4770c6ce..a86ddf0a 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -1,5 +1,11 @@ import time -from typing import Tuple, Optional # 增加了 Optional +from typing import Tuple, Optional, Union # 增加了 Optional +from src.plugins.memory_system.Hippocampus import HippocampusManager +from src.plugins.knowledge.knowledge_lib import qa_manager +from src.common.database import db +from src.plugins.chat.utils import get_embedding +# import jieba # 如果需要旧版知识库的回退,可能需要 +# import re # 如果需要旧版知识库的回退,可能需要 from src.common.logger_manager import get_logger from ..models.utils_model import LLMRequest from ...config.config import global_config @@ -21,20 +27,21 @@ PROMPT_INITIAL_REPLY = """{persona_text}。现在你在参与一场QQ私聊, 【当前对话目标】 {goals_str} -{knowledge_info_str} - 【最近行动历史概要】 {action_history_summary} +【你想起来的相关知识】 +{retrieved_knowledge_str} 【上一次行动的详细情况和结果】 {last_action_context} 【时间和超时提示】 -{time_since_last_bot_message_info}{timeout_context} +{time_since_last_bot_message_info}{timeout_context} 【最近的对话记录】(包括你已成功发送的消息 和 新收到的消息) {chat_history_text} +【你的的回忆】 +{retrieved_memory_str} ------ 可选行动类型以及解释: -fetch_knowledge: 需要调取知识或记忆,当需要专业知识或特定信息时选择,对方若提到你不太认识的人名或实体也可以尝试选择 listening: 倾听对方发言,当你认为对方话才说到一半,发言明显未结束时选择 direct_reply: 直接回复对方 rethink_goal: 思考一个对话目标,当你觉得目前对话需要目标,或当前目标不再适用,或话题卡住时选择。注意私聊的环境是灵活的,有可能需要经常选择 @@ -54,20 +61,20 @@ PROMPT_FOLLOW_UP = """{persona_text}。现在你在参与一场QQ私聊,刚刚 【当前对话目标】 {goals_str} -{knowledge_info_str} - 【最近行动历史概要】 {action_history_summary} +【你想起来的相关知识】 +{retrieved_knowledge_str} 【上一次行动的详细情况和结果】 {last_action_context} 【时间和超时提示】 {time_since_last_bot_message_info}{timeout_context} 【最近的对话记录】(包括你已成功发送的消息 和 新收到的消息) {chat_history_text} - +【你的的回忆】 +{retrieved_memory_str} ------ 可选行动类型以及解释: -fetch_knowledge: 需要调取知识,当需要专业知识或特定信息时选择,对方若提到你不太认识的人名或实体也可以尝试选择 wait: 暂时不说话,留给对方交互空间,等待对方回复(尤其是在你刚发言后、或上次发言因重复、发言过多被拒时、或不确定做什么时,这是不错的选择) listening: 倾听对方发言(虽然你刚发过言,但如果对方立刻回复且明显话没说完,可以选择这个) send_new_message: 发送一条新消息继续对话,允许适当的追问、补充、深入话题,或开启相关新话题。**但是避免在因重复被拒后立即使用,也不要在对方没有回复的情况下过多的“消息轰炸”或重复发言** @@ -117,7 +124,133 @@ class ActionPlanner: self.name = global_config.BOT_NICKNAME self.private_name = private_name self.chat_observer = ChatObserver.get_instance(stream_id, private_name) - # self.action_planner_info = ActionPlannerInfo() # 移除未使用的变量 + + async def _get_memory_info(self, text: str) -> str: + """根据文本自动检索相关记忆""" + memory_prompt = "" + related_memory_info = "" + try: + related_memory = await HippocampusManager.get_instance().get_memory_from_text( + text=text, + max_memory_num=2, # 最多获取 2 条记忆 + max_memory_length=2, # 每条记忆长度限制(这个参数含义可能需确认) + max_depth=3, # 搜索深度 + fast_retrieval=False # 是否快速检索 + ) + if related_memory: + for memory in related_memory: + # memory[0] 是记忆ID, memory[1] 是记忆内容 + related_memory_info += memory[1] + "\n" # 将记忆内容拼接起来 + if related_memory_info: + memory_prompt = f"你回忆起:\n{related_memory_info.strip()}\n(以上是你的回忆,供参考)\n" + logger.debug(f"[私聊]决策层[{self.private_name}]自动检索到记忆: {related_memory_info.strip()[:100]}...") + else: + logger.debug(f"[私聊]决策层[{self.private_name}]自动检索记忆返回为空。") + else: + logger.debug(f"[私聊]决策层[{self.private_name}]未自动检索到相关记忆。") + except Exception as e: + logger.error(f"[私聊]决策层[{self.private_name}]自动检索记忆时出错: {e}") + # memory_prompt = "检索记忆时出错。\n" # 可以选择是否提示错误 + return memory_prompt + + async def _get_prompt_info_old(self, message: str, threshold: float) -> str: + """ + 旧版的知识检索方法,根据消息文本从旧知识库(knowledges collection)检索。 + (移植并自 heartflow_prompt_builder.py) + """ + related_info = "" + start_time = time.time() + logger.debug(f"[私聊]决策层[{self.private_name}]开始使用旧版知识检索,消息: {message[:30]}...") + + # 简化处理:直接使用整个消息进行查询,不再提取主题 + query_text = message.strip() + if not query_text: + logger.debug(f"[私聊]决策层[{self.private_name}]旧版知识检索:消息为空,跳过。") + return "" + + embedding = None + try: + embedding = await get_embedding(query_text, request_type="pfc_implicit_knowledge") + except Exception as e: + logger.error(f"[私聊]决策层[{self.private_name}]旧版知识检索:获取嵌入向量时出错: {str(e)}") + + if not embedding: + logger.error(f"[私聊]决策层[{self.private_name}]旧版知识检索:获取嵌入向量失败。") + return "" + + # 调用我们之前添加的 get_info_from_db 函数 + results = get_info_from_db(embedding, limit=5, threshold=threshold, return_raw=True) # 最多查 5 条 + + logger.info(f"[私聊][{self.private_name}]旧版知识库查询完成,耗时: {time.time() - start_time:.3f}秒,获取{len(results)}条结果") + + # 去重和格式化 + unique_contents = set() + final_results_content = [] + for result in results: + content = result.get("content", "").strip() + similarity = result.get("similarity", 0.0) + if content and content not in unique_contents: + unique_contents.add(content) + # 可以选择性地加入相似度信息,或者只加内容 + # final_results_content.append(f"[{similarity:.2f}] {content}") + final_results_content.append(content) + + if final_results_content: + related_info = "\n".join(final_results_content) + logger.debug(f"[私聊][{self.private_name}]旧版知识检索格式化后内容: {related_info[:100]}...") + else: + logger.debug(f"[私聊][{self.private_name}]旧版知识检索未找到合适结果或结果为空。") + + logger.info(f"[私聊][{self.private_name}]旧版知识检索总耗时: {time.time() - start_time:.3f}秒") + return related_info + + async def _get_prompt_info(self, message: str, threshold: float = 0.38) -> str: + """ + 自动检索相关知识的主函数。优先使用 LPMM,失败则回退到旧版。 + (移植自 heartflow_prompt_builder.py) + """ + related_info = "" + start_time = time.time() + message = message.strip() + if not message: + logger.debug(f"[私聊][{self.private_name}]自动知识检索:输入消息为空。") + return "" + + logger.debug(f"[私聊][{self.private_name}]开始自动知识检索,消息: {message[:30]}...") + + # 1. 尝试从 LPMM 知识库获取知识 + try: + found_knowledge_from_lpmm = qa_manager.get_knowledge(message) + if found_knowledge_from_lpmm and found_knowledge_from_lpmm.strip(): + related_info = found_knowledge_from_lpmm.strip() + logger.info(f"[私聊][{self.private_name}]从 LPMM 知识库获取到知识,长度: {len(related_info)}") + logger.debug(f"[私聊][{self.private_name}]LPMM 知识内容: {related_info[:100]}...") + # LPMM 成功获取,直接返回 + logger.info(f"[私聊][{self.private_name}]自动知识检索(LPMM)耗时: {time.time() - start_time:.3f}秒") + return related_info + else: + logger.debug(f"[私聊][{self.private_name}]LPMM 知识库未返回有效知识,尝试旧版数据库检索。") + except Exception as e: + logger.error(f"[私聊][{self.private_name}]调用 LPMM 知识库 (qa_manager.get_knowledge) 时发生异常: {str(e)},尝试旧版数据库检索。") + + # 2. 如果 LPMM 失败或无结果,尝试旧版数据库 + try: + knowledge_from_old = await self._get_prompt_info_old(message, threshold=threshold) + if knowledge_from_old and knowledge_from_old.strip(): + related_info = knowledge_from_old.strip() + logger.info(f"[私聊][{self.private_name}]从旧版数据库检索到知识,长度: {len(related_info)}") + # 旧版成功获取,返回 + logger.info(f"[私聊][{self.private_name}]自动知识检索(旧版)耗时: {time.time() - start_time:.3f}秒") + return related_info + else: + logger.debug(f"[私聊][{self.private_name}]旧版数据库也未检索到有效知识。") + + except Exception as e2: + logger.error(f"[私聊][{self.private_name}]调用旧版知识库检索 (_get_prompt_info_old) 时也发生异常: {str(e2)}") + + # 如果两种方法都失败或无结果 + logger.info(f"[私聊][{self.private_name}]自动知识检索总耗时: {time.time() - start_time:.3f}秒,未找到任何相关知识。") + return "" # 返回空字符串 # 修改 plan 方法签名,增加 last_successful_reply_action 参数 async def plan( @@ -468,7 +601,6 @@ class ActionPlanner: valid_actions = [ "direct_reply", "send_new_message", - "fetch_knowledge", "wait", "listening", "rethink_goal", @@ -489,3 +621,83 @@ class ActionPlanner: # 外层异常处理保持不变 logger.error(f"[私聊][{self.private_name}]规划行动时调用 LLM 或处理结果出错: {str(e)}") return "wait", f"行动规划处理中发生错误,暂时等待: {str(e)}" + +def get_info_from_db( + query_embedding: list, limit: int = 1, threshold: float = 0.5, return_raw: bool = False +) -> Union[str, list]: + """ + 从旧知识库 (knowledges collection) 中根据嵌入向量相似度检索信息。 + (移植自 heartflow_prompt_builder.py) + """ + if not query_embedding: + return "" if not return_raw else [] + # 使用余弦相似度计算 + pipeline = [ + { + "$addFields": { + "dotProduct": { + "$reduce": { + "input": {"$range": [0, {"$size": "$embedding"}]}, + "initialValue": 0, + "in": { + "$add": [ + "$$value", + { + "$multiply": [ + {"$arrayElemAt": ["$embedding", "$$this"]}, + {"$arrayElemAt": [query_embedding, "$$this"]}, + ] + }, + ] + }, + } + }, + "magnitude1": { + "$sqrt": { + "$reduce": { + "input": "$embedding", + "initialValue": 0, + "in": {"$add": ["$$value", {"$multiply": ["$$this", "$$this"]}]}, + } + } + }, + "magnitude2": { + "$sqrt": { + "$reduce": { + "input": query_embedding, + "initialValue": 0, + "in": {"$add": ["$$value", {"$multiply": ["$$this", "$$this"]}]}, + } + } + }, + } + }, + # 防止除以零错误,添加一个小的 epsilon + {"$addFields": {"similarity": {"$divide": ["$dotProduct", {"$max": [{"$multiply": ["$magnitude1", "$magnitude2"]}, 1e-9]}]}}}, + { + "$match": { + "similarity": {"$gte": threshold} # 只保留相似度大于等于阈值的结果 + } + }, + {"$sort": {"similarity": -1}}, + {"$limit": limit}, + {"$project": {"content": 1, "similarity": 1}}, + ] + + try: + results = list(db.knowledges.aggregate(pipeline)) + # 注意:这里的 logger 需要能访问到,或者在这个函数里获取 logger 实例 + # logger.debug(f"旧知识库查询结果数量: {len(results)}") # 暂时注释掉,避免 logger 未定义 + except Exception as e: + # logger.error(f"执行旧知识库聚合查询时出错: {e}") # 暂时注释掉 + results = [] + + if not results: + return "" if not return_raw else [] + + if return_raw: + return results + else: + # 返回所有找到的内容,用换行分隔 + return "\n".join(str(result["content"]) for result in results) + diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index 890f807c..1b2362f3 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -1,4 +1,15 @@ -from typing import Tuple, List, Dict, Any +# 用于访问记忆系统 +from src.plugins.memory_system.Hippocampus import HippocampusManager +# 用于访问新的知识库 (LPMM) +from src.plugins.knowledge.knowledge_lib import qa_manager +# 用于访问数据库 (旧知识库需要) +from src.common.database import db +# 用于获取文本的嵌入向量 (旧知识库需要) +from src.plugins.chat.utils import get_embedding +# 可能用于旧知识库提取主题 (如果需要回退到旧方法) +# import jieba # 如果报错说找不到 jieba,可能需要安装: pip install jieba +# import re # 正则表达式库,通常 Python 自带 +from typing import Tuple, List, Dict, Any,Union from src.common.logger import get_module_logger from ..models.utils_model import LLMRequest from ...config.config import global_config @@ -8,6 +19,7 @@ from src.individuality.individuality import Individuality from .observation_info import ObservationInfo from .conversation_info import ConversationInfo from src.plugins.utils.chat_message_builder import build_readable_messages +import time logger = get_module_logger("reply_generator") @@ -18,17 +30,21 @@ PROMPT_DIRECT_REPLY = """{persona_text}。现在你在参与一场QQ私聊,请 当前对话目标:{goals_str} -{knowledge_info_str} +你有以下这些知识: +{retrieved_knowledge_str} +请你**记住上面的知识**,在回复中有可能会用到。 最近的聊天记录: {chat_history_text} +{related_memory_info}。 + 请根据上述信息,结合聊天记录,回复对方。该回复应该: 1. 符合对话目标,以"你"的角度发言(不要自己与自己对话!) 2. 符合你的性格特征和身份细节 3. 通俗易懂,自然流畅,像正常聊天一样,简短(通常20字以内,除非特殊情况) -4. 可以适当利用相关知识,但不要生硬引用 +4. 可以适当利用相关知识和回忆,但**不要生硬引用**,若无必要,也可以不利用 5. 自然、得体,结合聊天记录逻辑合理,且没有重复表达同质内容 请注意把握聊天内容,不要回复的太有条理,可以有个性。请分清"你"和对方说的话,不要把"你"说的话当做对方说的话,这是你自己说的话。 @@ -43,17 +59,20 @@ PROMPT_SEND_NEW_MESSAGE = """{persona_text}。现在你在参与一场QQ私聊 当前对话目标:{goals_str} -{knowledge_info_str} +你有以下这些知识: +{retrieved_knowledge_str} +请你**记住上面的知识**,在发消息时有可能会用到。 最近的聊天记录: {chat_history_text} +{related_memory_info} 请根据上述信息,结合聊天记录,继续发一条新消息(例如对之前消息的补充,深入话题,或追问等等)。该消息应该: 1. 符合对话目标,以"你"的角度发言(不要自己与自己对话!) 2. 符合你的性格特征和身份细节 3. 通俗易懂,自然流畅,像正常聊天一样,简短(通常20字以内,除非特殊情况) -4. 可以适当利用相关知识,但不要生硬引用 +4. 可以适当利用相关知识和回忆,但**不要生硬引用**,若无必要,也可以不利用 5. 跟之前你发的消息自然的衔接,逻辑合理,且没有重复表达同质内容或部分重叠内容 请注意把握聊天内容,不用太有条理,可以有个性。请分清"你"和对方说的话,不要把"你"说的话当做对方说的话,这是你自己说的话。 @@ -97,7 +116,132 @@ class ReplyGenerator: self.private_name = private_name self.chat_observer = ChatObserver.get_instance(stream_id, private_name) self.reply_checker = ReplyChecker(stream_id, private_name) + async def _get_memory_info(self, text: str) -> str: + """根据文本自动检索相关记忆""" + memory_prompt = "" + related_memory_info = "" + try: + related_memory = await HippocampusManager.get_instance().get_memory_from_text( + text=text, + max_memory_num=2, # 最多获取 2 条记忆 + max_memory_length=2, # 每条记忆长度限制(这个参数含义可能需确认) + max_depth=3, # 搜索深度 + fast_retrieval=False # 是否快速检索 + ) + if related_memory: + for memory in related_memory: + # memory[0] 是记忆ID, memory[1] 是记忆内容 + related_memory_info += memory[1] + "\n" # 将记忆内容拼接起来 + if related_memory_info: + memory_prompt = f"你回忆起:\n{related_memory_info.strip()}\n(以上是你的回忆,不一定是目前聊天里的人说的,回忆中别人说的事情也不一定是准确的,请记住)\n" + logger.debug(f"[私聊][{self.private_name}]自动检索到记忆: {related_memory_info.strip()[:100]}...") + else: + logger.debug(f"[私聊][{self.private_name}]自动检索记忆返回为空。") + else: + logger.debug(f"[私聊][{self.private_name}]未自动检索到相关记忆。") + except Exception as e: + logger.error(f"[私聊][{self.private_name}]自动检索记忆时出错: {e}") + # memory_prompt = "检索记忆时出错。\n" # 可以选择是否提示错误 + return memory_prompt + async def _get_prompt_info_old(self, message: str, threshold: float) -> str: + """ + 旧版的知识检索方法,根据消息文本从旧知识库(knowledges collection)检索。 + (移植并简化自 heartflow_prompt_builder.py) + """ + related_info = "" + start_time = time.time() + logger.debug(f"[私聊][{self.private_name}]开始使用旧版知识检索,消息: {message[:30]}...") + + # 简化处理:直接使用整个消息进行查询,不再提取主题 + query_text = message.strip() + if not query_text: + logger.debug(f"[私聊][{self.private_name}]旧版知识检索:消息为空,跳过。") + return "" + + embedding = None + try: + embedding = await get_embedding(query_text, request_type="pfc_implicit_knowledge") + except Exception as e: + logger.error(f"[私聊][{self.private_name}]旧版知识检索:获取嵌入向量时出错: {str(e)}") + + if not embedding: + logger.error(f"[私聊][{self.private_name}]旧版知识检索:获取嵌入向量失败。") + return "" + + # 调用我们之前添加的 get_info_from_db 函数 + results = get_info_from_db(embedding, limit=5, threshold=threshold, return_raw=True) # 最多查 5 条 + + logger.info(f"[私聊][{self.private_name}]旧版知识库查询完成,耗时: {time.time() - start_time:.3f}秒,获取{len(results)}条结果") + + # 去重和格式化 + unique_contents = set() + final_results_content = [] + for result in results: + content = result.get("content", "").strip() + similarity = result.get("similarity", 0.0) + if content and content not in unique_contents: + unique_contents.add(content) + # 可以选择性地加入相似度信息,或者只加内容 + # final_results_content.append(f"[{similarity:.2f}] {content}") + final_results_content.append(content) + + if final_results_content: + related_info = "\n".join(final_results_content) + logger.debug(f"[私聊][{self.private_name}]旧版知识检索格式化后内容: {related_info[:100]}...") + else: + logger.debug(f"[私聊][{self.private_name}]旧版知识检索未找到合适结果或结果为空。") + + logger.info(f"[私聊][{self.private_name}]旧版知识检索总耗时: {time.time() - start_time:.3f}秒") + return related_info + + async def _get_prompt_info(self, message: str, threshold: float = 0.38) -> str: + """ + 自动检索相关知识的主函数。优先使用 LPMM,失败则回退到旧版。 + (移植自 heartflow_prompt_builder.py) + """ + related_info = "" + start_time = time.time() + message = message.strip() + if not message: + logger.debug(f"[私聊][{self.private_name}]自动知识检索:输入消息为空。") + return "" + + logger.debug(f"[私聊][{self.private_name}]开始自动知识检索,消息: {message[:30]}...") + + # 1. 尝试从 LPMM 知识库获取知识 + try: + found_knowledge_from_lpmm = qa_manager.get_knowledge(message) + if found_knowledge_from_lpmm and found_knowledge_from_lpmm.strip(): + related_info = found_knowledge_from_lpmm.strip() + logger.info(f"[私聊][{self.private_name}]从 LPMM 知识库获取到知识,长度: {len(related_info)}") + logger.debug(f"[私聊][{self.private_name}]LPMM 知识内容: {related_info[:100]}...") + # LPMM 成功获取,直接返回 + logger.info(f"[私聊][{self.private_name}]自动知识检索(LPMM)耗时: {time.time() - start_time:.3f}秒") + return related_info + else: + logger.debug(f"[私聊][{self.private_name}]LPMM 知识库未返回有效知识,尝试旧版数据库检索。") + except Exception as e: + logger.error(f"[私聊][{self.private_name}]调用 LPMM 知识库 (qa_manager.get_knowledge) 时发生异常: {str(e)},尝试旧版数据库检索。") + + # 2. 如果 LPMM 失败或无结果,尝试旧版数据库 + try: + knowledge_from_old = await self._get_prompt_info_old(message, threshold=threshold) + if knowledge_from_old and knowledge_from_old.strip(): + related_info = knowledge_from_old.strip() + logger.info(f"[私聊][{self.private_name}]从旧版数据库检索到知识,长度: {len(related_info)}") + # 旧版成功获取,返回 + logger.info(f"[私聊][{self.private_name}]自动知识检索(旧版)耗时: {time.time() - start_time:.3f}秒") + return related_info + else: + logger.debug(f"[私聊][{self.private_name}]旧版数据库也未检索到有效知识。") + + except Exception as e2: + logger.error(f"[私聊][{self.private_name}]调用旧版知识库检索 (_get_prompt_info_old) 时也发生异常: {str(e2)}") + # 如果两种方法都失败或无结果 + logger.info(f"[私聊][{self.private_name}]自动知识检索总耗时: {time.time() - start_time:.3f}秒,未找到任何相关知识。") + return "" # 返回空字符串 + # 修改 generate 方法签名,增加 action_type 参数 async def generate( self, observation_info: ObservationInfo, conversation_info: ConversationInfo, action_type: str @@ -186,6 +330,36 @@ class ReplyGenerator: # 构建 Persona 文本 (persona_text) persona_text = f"你的名字是{self.name},{self.personality_info}。" + retrieved_memory_str = "" + retrieved_knowledge_str = "" + # 使用 chat_history_text 作为检索的上下文,因为它包含了最近的对话和新消息 + retrieval_context = chat_history_text + if retrieval_context and retrieval_context != "还没有聊天记录。" and retrieval_context != "[构建聊天记录出错]": + try: + # 提取记忆 + logger.debug(f"[私聊][{self.private_name}]开始自动检索记忆...") + retrieved_memory_str = await self._get_memory_info(text=retrieval_context) + if retrieved_memory_str: + logger.info(f"[私聊][{self.private_name}]自动检索到记忆片段。") + else: + logger.info(f"[私聊][{self.private_name}]未自动检索到相关记忆。") + + # 提取知识 + logger.debug(f"[私聊][{self.private_name}]开始自动检索知识...") + retrieved_knowledge_str = await self._get_prompt_info(message=retrieval_context) + if retrieved_knowledge_str: + logger.info(f"[私聊][{self.private_name}]自动检索到相关知识。") + else: + logger.info(f"[私聊][{self.private_name}]未自动检索到相关知识。") + + except Exception as retrieval_err: + logger.error(f"[私聊][{self.private_name}]在自动检索记忆/知识时发生错误: {retrieval_err}") + retrieved_memory_str = "检索记忆时出错。\n" + retrieved_knowledge_str = "检索知识时出错。\n" + else: + logger.debug(f"[私聊][{self.private_name}]聊天记录为空或无效,跳过自动记忆/知识检索。") + retrieved_memory_str = "无聊天记录,无法自动检索记忆。\n" + retrieved_knowledge_str = "无聊天记录,无法自动检索知识。\n" # --- 选择 Prompt --- if action_type == "send_new_message": @@ -204,6 +378,8 @@ class ReplyGenerator: goals_str=goals_str, chat_history_text=chat_history_text, knowledge_info_str=knowledge_info_str, + retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", # 如果为空则提示无 + retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。" # 如果为空则提示无 ) # --- 调用 LLM 生成 --- @@ -226,3 +402,82 @@ class ReplyGenerator: (此方法逻辑保持不变) """ return await self.reply_checker.check(reply, goal, chat_history, chat_history_str, retry_count) + +def get_info_from_db( + query_embedding: list, limit: int = 1, threshold: float = 0.5, return_raw: bool = False +) -> Union[str, list]: + """ + 从旧知识库 (knowledges collection) 中根据嵌入向量相似度检索信息。 + (移植自 heartflow_prompt_builder.py) + """ + if not query_embedding: + return "" if not return_raw else [] + # 使用余弦相似度计算 + pipeline = [ + { + "$addFields": { + "dotProduct": { + "$reduce": { + "input": {"$range": [0, {"$size": "$embedding"}]}, + "initialValue": 0, + "in": { + "$add": [ + "$$value", + { + "$multiply": [ + {"$arrayElemAt": ["$embedding", "$$this"]}, + {"$arrayElemAt": [query_embedding, "$$this"]}, + ] + }, + ] + }, + } + }, + "magnitude1": { + "$sqrt": { + "$reduce": { + "input": "$embedding", + "initialValue": 0, + "in": {"$add": ["$$value", {"$multiply": ["$$this", "$$this"]}]}, + } + } + }, + "magnitude2": { + "$sqrt": { + "$reduce": { + "input": query_embedding, + "initialValue": 0, + "in": {"$add": ["$$value", {"$multiply": ["$$this", "$$this"]}]}, + } + } + }, + } + }, + # 防止除以零错误,添加一个小的 epsilon + {"$addFields": {"similarity": {"$divide": ["$dotProduct", {"$max": [{"$multiply": ["$magnitude1", "$magnitude2"]}, 1e-9]}]}}}, + { + "$match": { + "similarity": {"$gte": threshold} # 只保留相似度大于等于阈值的结果 + } + }, + {"$sort": {"similarity": -1}}, + {"$limit": limit}, + {"$project": {"content": 1, "similarity": 1}}, + ] + + try: + results = list(db.knowledges.aggregate(pipeline)) + # 注意:这里的 logger 需要能访问到,或者在这个函数里获取 logger 实例 + # logger.debug(f"旧知识库查询结果数量: {len(results)}") # 暂时注释掉,避免 logger 未定义 + except Exception as e: + # logger.error(f"执行旧知识库聚合查询时出错: {e}") # 暂时注释掉 + results = [] + + if not results: + return "" if not return_raw else [] + + if return_raw: + return results + else: + # 返回所有找到的内容,用换行分隔 + return "\n".join(str(result["content"]) for result in results) From 974839c1b57de589ea63a411d38b36462cf637b9 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 2 May 2025 02:03:24 +0800 Subject: [PATCH 37/67] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E5=A4=8D=E5=BC=95?= =?UTF-8?q?=E7=94=A8=E5=9B=9E=E5=A4=8D=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + .../tool_can_use/lpmm_get_knowledge.py | 8 +- .../tool_can_use/rename_person_tool.py | 51 ++++---- src/heart_flow/background_tasks.py | 2 +- src/heart_flow/observation.py | 66 +++++++++++ src/heart_flow/sub_mind.py | 47 ++++---- src/heart_flow/subheartflow_manager.py | 2 +- src/plugins/chat/bot.py | 5 +- src/plugins/chat/message.py | 4 +- src/plugins/chat/message_sender.py | 11 +- src/plugins/chat/utils.py | 112 +++++------------- .../heartFC_chat/heartflow_prompt_builder.py | 1 + src/plugins/heartFC_chat/normal_chat.py | 20 +++- src/plugins/person_info/person_info.py | 18 +-- 14 files changed, 194 insertions(+), 155 deletions(-) diff --git a/.gitignore b/.gitignore index 88995ecc..5744424a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ NapCat.Framework.Windows.Once/ log/ logs/ tool_call_benchmark.py +run_maibot_core.bat +run_napcat_adapter.bat run_ad.bat llm_tool_benchmark_results.json MaiBot-Napcat-Adapter-main diff --git a/src/do_tool/tool_can_use/lpmm_get_knowledge.py b/src/do_tool/tool_can_use/lpmm_get_knowledge.py index 5eec260d..a4ded910 100644 --- a/src/do_tool/tool_can_use/lpmm_get_knowledge.py +++ b/src/do_tool/tool_can_use/lpmm_get_knowledge.py @@ -52,7 +52,7 @@ class SearchKnowledgeFromLPMMTool(BaseTool): except Exception as e: logger.error(f"知识库搜索工具执行失败: {str(e)}") # 在其他异常情况下,确保 id 仍然是 query (如果它被定义了) - query_id = query if 'query' in locals() else 'unknown_query' + query_id = query if "query" in locals() else "unknown_query" return {"type": "info", "id": query_id, "content": f"lpmm知识库搜索失败,炸了: {str(e)}"} # def get_info_from_db( @@ -143,13 +143,15 @@ class SearchKnowledgeFromLPMMTool(BaseTool): formatted_string = "我找到了一些相关知识:\n" for i, result in enumerate(results): - chunk_id = result.get("chunk_id") + # chunk_id = result.get("chunk_id") text = result.get("text", "") source = result.get("source", "未知来源") source_type = result.get("source_type", "未知类型") similarity = result.get("similarity", 0.0) - formatted_string += f"{i + 1}. (相似度: {similarity:.2f}) 类型: {source_type}, 来源: {source} \n内容片段: {text}\n\n" + formatted_string += ( + f"{i + 1}. (相似度: {similarity:.2f}) 类型: {source_type}, 来源: {source} \n内容片段: {text}\n\n" + ) # 暂时去掉chunk_id # formatted_string += f"{i + 1}. (相似度: {similarity:.2f}) 类型: {source_type}, 来源: {source}, Chunk ID: {chunk_id} \n内容片段: {text}\n\n" diff --git a/src/do_tool/tool_can_use/rename_person_tool.py b/src/do_tool/tool_can_use/rename_person_tool.py index 9e796931..d9f23cf4 100644 --- a/src/do_tool/tool_can_use/rename_person_tool.py +++ b/src/do_tool/tool_can_use/rename_person_tool.py @@ -1,28 +1,24 @@ from src.do_tool.tool_can_use.base_tool import BaseTool, register_tool from src.plugins.person_info.person_info import person_info_manager from src.common.logger_manager import get_logger -from src.plugins.person_info.relationship_manager import relationship_manager -import json import time logger = get_logger("rename_person_tool") + class RenamePersonTool(BaseTool): name = "rename_person" description = "这个工具可以改变用户的昵称。你可以选择改变对他人的称呼。" parameters = { "type": "object", "properties": { - "person_name": { - "type": "string", - "description": "需要重新取名的用户的当前昵称" - }, + "person_name": {"type": "string", "description": "需要重新取名的用户的当前昵称"}, "message_content": { "type": "string", - "description": "可选的。当前的聊天内容或特定要求,用于提供取名建议的上下文。" - } + "description": "可选的。当前的聊天内容或特定要求,用于提供取名建议的上下文。", + }, }, - "required": ["person_name"] + "required": ["person_name"], } async def execute(self, function_args: dict, message_txt=""): @@ -37,13 +33,10 @@ class RenamePersonTool(BaseTool): dict: 包含执行结果的字典 """ person_name_to_find = function_args.get("person_name") - request_context = function_args.get("message_content", "") # 如果没有提供,则为空字符串 + request_context = function_args.get("message_content", "") # 如果没有提供,则为空字符串 if not person_name_to_find: - return { - "name": self.name, - "content": "错误:必须提供需要重命名的用户昵称 (person_name)。" - } + return {"name": self.name, "content": "错误:必须提供需要重命名的用户昵称 (person_name)。"} try: # 1. 根据昵称查找用户信息 @@ -54,35 +47,34 @@ class RenamePersonTool(BaseTool): logger.info(f"未找到昵称为 '{person_name_to_find}' 的用户。") return { "name": self.name, - "content": f"找不到昵称为 '{person_name_to_find}' 的用户。请确保输入的是我之前为该用户取的昵称。" + "content": f"找不到昵称为 '{person_name_to_find}' 的用户。请确保输入的是我之前为该用户取的昵称。", } person_id = person_info.get("person_id") - user_nickname = person_info.get("nickname") # 这是用户原始昵称 + user_nickname = person_info.get("nickname") # 这是用户原始昵称 user_cardname = person_info.get("user_cardname") user_avatar = person_info.get("user_avatar") if not person_id: - logger.error(f"找到了用户 '{person_name_to_find}' 但无法获取 person_id") - return { - "name": self.name, - "content": f"找到了用户 '{person_name_to_find}' 但获取内部ID时出错。" - } + logger.error(f"找到了用户 '{person_name_to_find}' 但无法获取 person_id") + return {"name": self.name, "content": f"找到了用户 '{person_name_to_find}' 但获取内部ID时出错。"} # 2. 调用 qv_person_name 进行取名 - logger.debug(f"为用户 {person_id} (原昵称: {person_name_to_find}) 调用 qv_person_name,请求上下文: '{request_context}'") + logger.debug( + f"为用户 {person_id} (原昵称: {person_name_to_find}) 调用 qv_person_name,请求上下文: '{request_context}'" + ) result = await person_info_manager.qv_person_name( person_id=person_id, user_nickname=user_nickname, user_cardname=user_cardname, user_avatar=user_avatar, - request=request_context + request=request_context, ) # 3. 处理结果 if result and result.get("nickname"): new_name = result["nickname"] - reason = result.get("reason", "未提供理由") + # reason = result.get("reason", "未提供理由") logger.info(f"成功为用户 {person_id} 取了新昵称: {new_name}") content = f"已成功将用户 {person_name_to_find} 的备注名更新为 {new_name}" @@ -93,14 +85,14 @@ class RenamePersonTool(BaseTool): # 尝试从内存中获取可能已经更新的名字 current_name = await person_info_manager.get_value(person_id, "person_name") if current_name and current_name != person_name_to_find: - return { + return { "name": self.name, - "content": f"尝试取新昵称时遇到一点小问题,但我已经将 '{person_name_to_find}' 的昵称更新为 '{current_name}' 了。" + "content": f"尝试取新昵称时遇到一点小问题,但我已经将 '{person_name_to_find}' 的昵称更新为 '{current_name}' 了。", } else: - return { + return { "name": self.name, - "content": f"尝试为 '{person_name_to_find}' 取新昵称时遇到了问题,未能成功生成。可能需要稍后再试。" + "content": f"尝试为 '{person_name_to_find}' 取新昵称时遇到了问题,未能成功生成。可能需要稍后再试。", } except Exception as e: @@ -108,5 +100,6 @@ class RenamePersonTool(BaseTool): logger.error(error_msg, exc_info=True) return {"type": "info_error", "id": f"rename_error_{time.time()}", "content": error_msg} + # 注册工具 -register_tool(RenamePersonTool) \ No newline at end of file +register_tool(RenamePersonTool) diff --git a/src/heart_flow/background_tasks.py b/src/heart_flow/background_tasks.py index c1730d5a..301c2984 100644 --- a/src/heart_flow/background_tasks.py +++ b/src/heart_flow/background_tasks.py @@ -18,7 +18,7 @@ INTEREST_EVAL_INTERVAL_SECONDS = 5 # 新增聊天超时检查间隔 NORMAL_CHAT_TIMEOUT_CHECK_INTERVAL_SECONDS = 60 # 新增状态评估间隔 -HF_JUDGE_STATE_UPDATE_INTERVAL_SECONDS = 60 +HF_JUDGE_STATE_UPDATE_INTERVAL_SECONDS = 20 # 新增私聊激活检查间隔 PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS = 5 # 与兴趣评估类似,设为5秒 diff --git a/src/heart_flow/observation.py b/src/heart_flow/observation.py index 0c63e751..2d819a88 100644 --- a/src/heart_flow/observation.py +++ b/src/heart_flow/observation.py @@ -14,6 +14,8 @@ from src.plugins.utils.chat_message_builder import ( ) from src.plugins.utils.prompt_builder import Prompt, global_prompt_manager from typing import Optional +import difflib +from src.plugins.chat.message import MessageRecv # 添加 MessageRecv 导入 # Import the new utility function from .utils_chat import get_chat_type_and_target_info @@ -227,6 +229,70 @@ class ChattingObservation(Observation): f"Chat {self.chat_id} - 压缩早期记忆:{self.mid_memory_info}\n现在聊天内容:{self.talking_message_str}" ) + async def find_best_matching_message(self, search_str: str, min_similarity: float = 0.6) -> Optional[MessageRecv]: + """ + 在 talking_message 中查找与 search_str 最匹配的消息。 + + Args: + search_str: 要搜索的字符串。 + min_similarity: 要求的最低相似度(0到1之间)。 + + Returns: + 匹配的 MessageRecv 实例,如果找不到则返回 None。 + """ + best_match_score = -1.0 + best_match_dict = None + + if not self.talking_message: + logger.debug(f"Chat {self.chat_id}: talking_message is empty, cannot find match for '{search_str}'") + return None + + for message_dict in self.talking_message: + try: + # 临时创建 MessageRecv 以处理文本 + temp_msg = MessageRecv(message_dict) + await temp_msg.process() # 处理消息以获取 processed_plain_text + current_text = temp_msg.processed_plain_text + + if not current_text: # 跳过没有文本内容的消息 + continue + + # 计算相似度 + matcher = difflib.SequenceMatcher(None, search_str, current_text) + score = matcher.ratio() + + # logger.debug(f"Comparing '{search_str}' with '{current_text}', score: {score}") # 可选:用于调试 + + if score > best_match_score: + best_match_score = score + best_match_dict = message_dict + + except Exception as e: + logger.error(f"Error processing message for matching in chat {self.chat_id}: {e}", exc_info=True) + continue # 继续处理下一条消息 + + if best_match_dict is not None and best_match_score >= min_similarity: + logger.debug(f"Found best match for '{search_str}' with score {best_match_score:.2f}") + try: + final_msg = MessageRecv(best_match_dict) + await final_msg.process() + # 确保 MessageRecv 实例有关联的 chat_stream + if hasattr(self, "chat_stream"): + final_msg.update_chat_stream(self.chat_stream) + else: + logger.warning( + f"ChattingObservation instance for chat {self.chat_id} does not have a chat_stream attribute set." + ) + return final_msg + except Exception as e: + logger.error(f"Error creating final MessageRecv for chat {self.chat_id}: {e}", exc_info=True) + return None + else: + logger.debug( + f"No suitable match found for '{search_str}' in chat {self.chat_id} (best score: {best_match_score:.2f}, threshold: {min_similarity})" + ) + return None + async def has_new_messages_since(self, timestamp: float) -> bool: """检查指定时间戳之后是否有新消息""" count = num_new_messages_since(chat_id=self.chat_id, timestamp_start=timestamp) diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index 0dfc836c..1275fbbf 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -1,4 +1,4 @@ -from .observation import Observation, ChattingObservation +from .observation import ChattingObservation from src.plugins.models.utils_model import LLMRequest from src.config.config import global_config import time @@ -146,10 +146,9 @@ class SubMind: lines = ["【信息】"] for item in self.structured_info: # 简化展示,突出内容和类型,包含TTL供调试 - type_str = item.get('type', '未知类型') - content_str = item.get('content', '') - ttl = item.get('ttl', '?') - + type_str = item.get("type", "未知类型") + content_str = item.get("content", "") + if type_str == "info": lines.append(f"刚刚: {content_str}") elif type_str == "memory": @@ -178,18 +177,24 @@ class SubMind: # ---------- 0. 更新和清理 structured_info ---------- if self.structured_info: - logger.debug(f"{self.log_prefix} 更新前的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}") + logger.debug( + f"{self.log_prefix} 更新前的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}" + ) updated_info = [] for item in self.structured_info: - item['ttl'] -= 1 - if item['ttl'] > 0: + item["ttl"] -= 1 + if item["ttl"] > 0: updated_info.append(item) else: logger.debug(f"{self.log_prefix} 移除过期的 structured_info 项: {item['id']}") self.structured_info = updated_info - logger.debug(f"{self.log_prefix} 更新后的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}") + logger.debug( + f"{self.log_prefix} 更新后的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}" + ) self._update_structured_info_str() - logger.debug(f"{self.log_prefix} 当前完整的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}") + logger.debug( + f"{self.log_prefix} 当前完整的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}" + ) # ---------- 1. 准备基础数据 ---------- # 获取现有想法和情绪状态 @@ -202,10 +207,10 @@ class SubMind: logger.error(f"{self.log_prefix} 无法获取有效的观察对象或缺少聊天类型信息") self.update_current_mind("(观察出错了...)") return self.current_mind, self.past_mind - + is_group_chat = observation.is_group_chat # logger.debug(f"is_group_chat: {is_group_chat}") - + chat_target_info = observation.chat_target_info chat_target_name = "对方" # Default for private if not is_group_chat and chat_target_info: @@ -496,7 +501,7 @@ class SubMind: tool_instance: 工具使用器实例 """ tool_results = [] - new_structured_items = [] # 收集新产生的结构化信息 + new_structured_items = [] # 收集新产生的结构化信息 # 执行所有工具调用 for tool_call in tool_calls: @@ -506,26 +511,26 @@ class SubMind: tool_results.append(result) # 创建新的结构化信息项 new_item = { - "type": result.get("type", "unknown_type"), # 使用 'type' 键 - "id": result.get("id", f"fallback_id_{time.time()}"), # 使用 'id' 键 - "content": result.get("content", ""), # 'content' 键保持不变 - "ttl": 3 + "type": result.get("type", "unknown_type"), # 使用 'type' 键 + "id": result.get("id", f"fallback_id_{time.time()}"), # 使用 'id' 键 + "content": result.get("content", ""), # 'content' 键保持不变 + "ttl": 3, } new_structured_items.append(new_item) except Exception as tool_e: logger.error(f"[{self.subheartflow_id}] 工具执行失败: {tool_e}") - logger.error(traceback.format_exc()) # 添加 traceback 记录 + logger.error(traceback.format_exc()) # 添加 traceback 记录 # 如果有新的工具结果,记录并更新结构化信息 if new_structured_items: - self.structured_info.extend(new_structured_items) # 添加到现有列表 + self.structured_info.extend(new_structured_items) # 添加到现有列表 logger.debug(f"工具调用收集到新的结构化信息: {safe_json_dumps(new_structured_items, ensure_ascii=False)}") # logger.debug(f"当前完整的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}") # 可以取消注释以查看完整列表 - self._update_structured_info_str() # 添加新信息后,更新字符串表示 + self._update_structured_info_str() # 添加新信息后,更新字符串表示 def update_current_mind(self, response): - if self.current_mind: # 只有当 current_mind 非空时才添加到 past_mind + if self.current_mind: # 只有当 current_mind 非空时才添加到 past_mind self.past_mind.append(self.current_mind) # 可以考虑限制 past_mind 的大小,例如: # max_past_mind_size = 10 diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index fa8aaa96..f06a68c8 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -128,7 +128,7 @@ class SubHeartflowManager: # 添加聊天观察者 observation = ChattingObservation(chat_id=subheartflow_id) await observation.initialize() - + new_subflow.add_observation(observation) # 注册子心流 diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index dd65ca1c..9c4a3358 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -82,15 +82,14 @@ class ChatBot: if userinfo.user_id in global_config.ban_user_id: logger.debug(f"用户{userinfo.user_id}被禁止回复") return - + if groupinfo is None: logger.trace("检测到私聊消息,检查") - # 好友黑名单拦截 + # 好友黑名单拦截 if userinfo.user_id not in global_config.talk_allowed_private: logger.debug(f"用户{userinfo.user_id}没有私聊权限") return - # 群聊黑名单拦截 if groupinfo is not None and groupinfo.group_id not in global_config.talk_allowed_groups: logger.trace(f"群{groupinfo.group_id}被禁止回复") diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py index a29d0ab2..a0ff33ab 100644 --- a/src/plugins/chat/message.py +++ b/src/plugins/chat/message.py @@ -294,7 +294,9 @@ class MessageSending(MessageProcessBase): def set_reply(self, reply: Optional["MessageRecv"] = None): """设置回复消息""" - if self.message_info.format_info is not None and "reply" in self.message_info.format_info.accept_format: + # print(f"set_reply: {reply}") + # if self.message_info.format_info is not None and "reply" in self.message_info.format_info.accept_format: + if True: if reply: self.reply = reply if self.reply: diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index c159a4bb..8663be56 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -209,24 +209,31 @@ class MessageManager: _ = message.update_thinking_time() # 更新思考时间 thinking_start_time = message.thinking_start_time now_time = time.time() + logger.debug(f"thinking_start_time:{thinking_start_time},now_time:{now_time}") thinking_messages_count, thinking_messages_length = count_messages_between( start_time=thinking_start_time, end_time=now_time, stream_id=message.chat_stream.stream_id ) + # print(f"message.reply:{message.reply}") # --- 条件应用 set_reply 逻辑 --- + logger.debug( + f"[message.apply_set_reply_logic:{message.apply_set_reply_logic},message.is_head:{message.is_head},thinking_messages_count:{thinking_messages_count},thinking_messages_length:{thinking_messages_length},message.is_private_message():{message.is_private_message()}]" + ) if ( message.apply_set_reply_logic # 检查标记 and message.is_head - and (thinking_messages_count > 4 or thinking_messages_length > 250) + and (thinking_messages_count > 1 or thinking_messages_length > 20) and not message.is_private_message() ): logger.debug( f"[{message.chat_stream.stream_id}] 应用 set_reply 逻辑: {message.processed_plain_text[:20]}..." ) - message.set_reply() + message.set_reply(message.reply) # --- 结束条件 set_reply --- await message.process() # 预处理消息内容 + + logger.debug(f"{message}") # 使用全局 message_sender 实例 await send_message(message) diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index 9b71ecc5..53e8f6f6 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -6,13 +6,13 @@ from collections import Counter import jieba import numpy as np from src.common.logger import get_module_logger +from pymongo.errors import PyMongoError 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 import MessageRecv from maim_message import UserInfo -from .chat_stream import ChatStream from ..moods.moods import MoodManager from ...common.database import db @@ -107,8 +107,6 @@ async def get_embedding(text, request_type="embedding"): return embedding - - def get_recent_group_detailed_plain_text(chat_stream_id: str, limit: int = 12, combine=False): recent_messages = list( db.messages.find( @@ -566,93 +564,45 @@ def count_messages_between(start_time: float, end_time: float, stream_id: str) - """计算两个时间点之间的消息数量和文本总长度 Args: - start_time (float): 起始时间戳 - end_time (float): 结束时间戳 + start_time (float): 起始时间戳 (不包含) + end_time (float): 结束时间戳 (包含) stream_id (str): 聊天流ID Returns: tuple[int, int]: (消息数量, 文本总长度) - - 消息数量:包含起始时间的消息,不包含结束时间的消息 - - 文本总长度:所有消息的processed_plain_text长度之和 """ + count = 0 + total_length = 0 + + # 参数校验 (可选但推荐) + if start_time >= end_time: + # logger.debug(f"开始时间 {start_time} 大于或等于结束时间 {end_time},返回 0, 0") + return 0, 0 + if not stream_id: + logger.error("stream_id 不能为空") + return 0, 0 + + # 直接查询时间范围内的消息 + # time > start_time AND time <= end_time + query = {"chat_id": stream_id, "time": {"$gt": start_time, "$lte": end_time}} + try: - # 获取开始时间之前最新的一条消息 - start_message = db.messages.find_one( - {"chat_id": stream_id, "time": {"$lte": start_time}}, - sort=[("time", -1), ("_id", -1)], # 按时间倒序,_id倒序(最后插入的在前) - ) + # 执行查询 + messages_cursor = db.messages.find(query) - # 获取结束时间最近的一条消息 - # 先找到结束时间点的所有消息 - end_time_messages = list( - db.messages.find( - {"chat_id": stream_id, "time": {"$lte": end_time}}, - sort=[("time", -1)], # 先按时间倒序 - ).limit(10) - ) # 限制查询数量,避免性能问题 - - if not end_time_messages: - logger.warning(f"未找到结束时间 {end_time} 之前的消息") - return 0, 0 - - # 找到最大时间 - max_time = end_time_messages[0]["time"] - # 在最大时间的消息中找最后插入的(_id最大的) - end_message = max([msg for msg in end_time_messages if msg["time"] == max_time], key=lambda x: x["_id"]) - - if not start_message: - logger.warning(f"未找到开始时间 {start_time} 之前的消息") - return 0, 0 - - # 调试输出 - # print("\n=== 消息范围信息 ===") - # print("Start message:", { - # "message_id": start_message.get("message_id"), - # "time": start_message.get("time"), - # "text": start_message.get("processed_plain_text", ""), - # "_id": str(start_message.get("_id")) - # }) - # print("End message:", { - # "message_id": end_message.get("message_id"), - # "time": end_message.get("time"), - # "text": end_message.get("processed_plain_text", ""), - # "_id": str(end_message.get("_id")) - # }) - # print("Stream ID:", stream_id) - - # 如果结束消息的时间等于开始时间,返回0 - if end_message["time"] == start_message["time"]: - return 0, 0 - - # 获取并打印这个时间范围内的所有消息 - # print("\n=== 时间范围内的所有消息 ===") - all_messages = list( - db.messages.find( - {"chat_id": stream_id, "time": {"$gte": start_message["time"], "$lte": end_message["time"]}}, - sort=[("time", 1), ("_id", 1)], # 按时间正序,_id正序 - ) - ) - - count = 0 - total_length = 0 - for msg in all_messages: + # 遍历结果计算数量和长度 + for msg in messages_cursor: count += 1 - text_length = len(msg.get("processed_plain_text", "")) - total_length += text_length - # print(f"\n消息 {count}:") - # print({ - # "message_id": msg.get("message_id"), - # "time": msg.get("time"), - # "text": msg.get("processed_plain_text", ""), - # "text_length": text_length, - # "_id": str(msg.get("_id")) - # }) + total_length += len(msg.get("processed_plain_text", "")) - # 如果时间不同,需要把end_message本身也计入 - return count - 1, total_length + # logger.debug(f"查询范围 ({start_time}, {end_time}] 内找到 {count} 条消息,总长度 {total_length}") + return count, total_length - except Exception as e: - logger.error(f"计算消息数量时出错: {str(e)}") + except PyMongoError as e: + logger.error(f"查询 stream_id={stream_id} 在 ({start_time}, {end_time}] 范围内的消息时出错: {e}") + return 0, 0 + except Exception as e: # 保留一个通用异常捕获以防万一 + logger.error(f"计算消息数量时发生意外错误: {e}") return 0, 0 diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 92346c93..c59168a7 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -372,6 +372,7 @@ class PromptBuilder: memory_prompt = await global_prompt_manager.format_prompt( "memory_prompt", related_memory_info=related_memory_info ) + message_list_before_now = get_raw_msg_before_timestamp_with_chat( chat_id=chat_stream.stream_id, timestamp=time.time(), diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py index 5b5e8e88..70568f83 100644 --- a/src/plugins/heartFC_chat/normal_chat.py +++ b/src/plugins/heartFC_chat/normal_chat.py @@ -86,11 +86,12 @@ class NormalChat: bot_user_info=bot_user_info, reply=message, thinking_start_time=thinking_time_point, - timestamp=timestamp if timestamp is not None else None + timestamp=timestamp if timestamp is not None else None, ) await message_manager.add_message(thinking_message) return thinking_id + # 改为实例方法 async def _add_messages_to_manager( self, message: MessageRecv, response_set: List[str], thinking_id @@ -206,7 +207,10 @@ class NormalChat: try: # 处理消息 await self.normal_response( - message=message, is_mentioned=is_mentioned, interested_rate=interest_value, rewind_response = False + message=message, + is_mentioned=is_mentioned, + interested_rate=interest_value, + rewind_response=False, ) except Exception as e: logger.error(f"[{self.stream_name}] 处理兴趣消息{msg_id}时出错: {e}\n{traceback.format_exc()}") @@ -214,7 +218,9 @@ class NormalChat: self.interest_dict.pop(msg_id, None) # 改为实例方法, 移除 chat 参数 - async def normal_response(self, message: MessageRecv, is_mentioned: bool, interested_rate: float, rewind_response: bool = False) -> None: + async def normal_response( + self, message: MessageRecv, is_mentioned: bool, interested_rate: float, rewind_response: bool = False + ) -> None: # 检查收到的消息是否属于当前实例处理的 chat stream if message.chat_stream.stream_id != self.stream_id: logger.error( @@ -393,13 +399,17 @@ class NormalChat: try: logger.info(f"[{self.stream_name}] 处理初始高兴趣消息 {msg_id} (兴趣值: {interest_value:.2f})") - await self.normal_response(message=message, is_mentioned=is_mentioned, interested_rate=interest_value, rewind_response = True) + await self.normal_response( + message=message, is_mentioned=is_mentioned, interested_rate=interest_value, rewind_response=True + ) processed_count += 1 except Exception as e: logger.error(f"[{self.stream_name}] 处理初始兴趣消息 {msg_id} 时出错: {e}\\n{traceback.format_exc()}") # --- 新增:处理完后清空整个字典 --- - logger.info(f"[{self.stream_name}] 处理了 {processed_count} 条初始高兴趣消息。现在清空所有剩余的初始兴趣消息...") + logger.info( + f"[{self.stream_name}] 处理了 {processed_count} 条初始高兴趣消息。现在清空所有剩余的初始兴趣消息..." + ) self.interest_dict.clear() # --- 新增结束 --- diff --git a/src/plugins/person_info/person_info.py b/src/plugins/person_info/person_info.py index e95fffdb..d4e69d7e 100644 --- a/src/plugins/person_info/person_info.py +++ b/src/plugins/person_info/person_info.py @@ -188,7 +188,9 @@ class PersonInfoManager: logger.warning(f"无法从文本中提取有效的JSON字典: {text}") return {"nickname": "", "reason": ""} - async def qv_person_name(self, person_id: str, user_nickname: str, user_cardname: str, user_avatar: str, request: str = ""): + async def qv_person_name( + self, person_id: str, user_nickname: str, user_cardname: str, user_avatar: str, request: str = "" + ): """给某个用户取名""" if not person_id: logger.debug("取名失败:person_id不能为空") @@ -214,7 +216,7 @@ class PersonInfoManager: qv_name_prompt += f"你之前叫他{old_name},是因为{old_reason}," qv_name_prompt += f"\n其他取名的要求是:{request}" - + qv_name_prompt += "\n请根据以上用户信息,想想你叫他什么比较好,请最好使用用户的qq昵称,可以稍作修改" if existing_names: qv_name_prompt += f"\n请注意,以下名称已被使用,不要使用以下昵称:{existing_names}。\n" @@ -526,7 +528,7 @@ class PersonInfoManager: for pid, name in self.person_name_list.items(): if name == person_name: found_person_id = pid - break # 找到第一个匹配就停止 + break # 找到第一个匹配就停止 if not found_person_id: # 如果内存没有,尝试数据库查询(可能内存未及时更新或启动时未加载) @@ -535,21 +537,21 @@ class PersonInfoManager: found_person_id = document.get("person_id") else: logger.debug(f"数据库中也未找到名为 '{person_name}' 的用户") - return None # 数据库也找不到 + return None # 数据库也找不到 # 根据找到的 person_id 获取所需信息 if found_person_id: required_fields = ["person_id", "platform", "user_id", "nickname", "user_cardname", "user_avatar"] person_data = await self.get_values(found_person_id, required_fields) - if person_data: # 确保 get_values 成功返回 + if person_data: # 确保 get_values 成功返回 return person_data else: logger.warning(f"找到了 person_id '{found_person_id}' 但获取详细信息失败") return None else: - # 这理论上不应该发生,因为上面已经处理了找不到的情况 - logger.error(f"逻辑错误:未能为 '{person_name}' 确定 person_id") - return None + # 这理论上不应该发生,因为上面已经处理了找不到的情况 + logger.error(f"逻辑错误:未能为 '{person_name}' 确定 person_id") + return None person_info_manager = PersonInfoManager() From a5368b44d0c993463e435b80385858120f523d15 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 1 May 2025 18:03:41 +0000 Subject: [PATCH 38/67] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/message_sender.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index 8663be56..ea52bab8 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -232,7 +232,7 @@ class MessageManager: # --- 结束条件 set_reply --- await message.process() # 预处理消息内容 - + logger.debug(f"{message}") # 使用全局 message_sender 实例 From 63361707b33a3c675059a23e035dca49529b29dd Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 2 May 2025 02:03:55 +0800 Subject: [PATCH 39/67] Update message_sender.py --- src/plugins/chat/message_sender.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index 8663be56..6971767f 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -222,7 +222,7 @@ class MessageManager: if ( message.apply_set_reply_logic # 检查标记 and message.is_head - and (thinking_messages_count > 1 or thinking_messages_length > 20) + and (thinking_messages_count > 3 or thinking_messages_length > 200) and not message.is_private_message() ): logger.debug( From 9cf62f983e00f9f6a7c1fdc005de2250c9d6762c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Fri, 2 May 2025 03:18:24 +0800 Subject: [PATCH 40/67] =?UTF-8?q?feat=EF=BC=9A=E4=B8=BA=E5=A4=9A=E4=B8=AA?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E6=B7=BB=E5=8A=A0=E4=B8=B0=E5=AF=8C=E7=9A=84?= =?UTF-8?q?=E8=BF=BD=E8=B8=AA=E4=BF=A1=E6=81=AF=E4=BB=A5=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E8=B0=83=E8=AF=95=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 2 ++ src/common/database.py | 2 ++ src/common/log_decorators.py | 2 ++ src/common/server.py | 2 ++ src/config/config.py | 2 ++ src/do_tool/tool_can_use/base_tool.py | 2 ++ src/individuality/individuality.py | 2 ++ src/individuality/offline_llm.py | 2 ++ src/main.py | 2 ++ src/plugins/PFC/chat_observer.py | 2 ++ src/plugins/PFC/conversation.py | 2 ++ src/plugins/PFC/message_sender.py | 2 ++ src/plugins/PFC/pfc.py | 2 ++ src/plugins/chat/chat_stream.py | 2 ++ src/plugins/chat/message.py | 2 ++ src/plugins/chat/message_sender.py | 2 ++ src/plugins/chat/utils_image.py | 2 ++ src/plugins/config_reload/api.py | 2 ++ src/plugins/emoji_system/emoji_manager.py | 2 ++ src/plugins/heartFC_chat/heartFC_chat.py | 2 ++ src/plugins/heartFC_chat/heartFC_sender.py | 2 ++ src/plugins/knowledge/src/embedding_store.py | 2 ++ src/plugins/memory_system/Hippocampus.py | 2 ++ src/plugins/memory_system/debug_memory.py | 2 ++ src/plugins/memory_system/manually_alter_memory.py | 2 ++ src/plugins/memory_system/offline_llm.py | 2 ++ src/plugins/memory_system/sample_distribution.py | 2 ++ src/plugins/models/utils_model.py | 2 ++ src/plugins/utils/prompt_builder.py | 2 ++ src/plugins/utils/timer_calculator.py | 2 ++ src/plugins/willing/willing_manager.py | 2 ++ src/plugins/zhishi/knowledge_library.py | 3 +++ 32 files changed, 65 insertions(+) diff --git a/bot.py b/bot.py index 9a4a5000..ecdbd2b9 100644 --- a/bot.py +++ b/bot.py @@ -13,6 +13,8 @@ from src.common.logger_manager import get_logger # from src.common.logger import LogConfig, CONFIRM_STYLE_CONFIG from src.common.crash_logger import install_crash_handler from src.main import MainSystem +from rich.traceback import install +install(show_locals=True, extra_lines=3) logger = get_logger("main") diff --git a/src/common/database.py b/src/common/database.py index ee0ead0b..846c4ee7 100644 --- a/src/common/database.py +++ b/src/common/database.py @@ -1,6 +1,8 @@ import os from pymongo import MongoClient from pymongo.database import Database +from rich.traceback import install +install(show_locals=True, extra_lines=3) _client = None _db = None diff --git a/src/common/log_decorators.py b/src/common/log_decorators.py index 9838717f..4e881984 100644 --- a/src/common/log_decorators.py +++ b/src/common/log_decorators.py @@ -2,6 +2,8 @@ import functools import inspect from typing import Callable, Any from .logger import logger, add_custom_style_handler +from rich.traceback import install +install(show_locals=True, extra_lines=3) def use_log_style( diff --git a/src/common/server.py b/src/common/server.py index 51799629..0511f443 100644 --- a/src/common/server.py +++ b/src/common/server.py @@ -2,6 +2,8 @@ from fastapi import FastAPI, APIRouter from typing import Optional from uvicorn import Config, Server as UvicornServer import os +from rich.traceback import install +install(show_locals=True, extra_lines=3) class Server: diff --git a/src/config/config.py b/src/config/config.py index f5be7ec6..29c28c83 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -14,6 +14,8 @@ from packaging.version import Version, InvalidVersion from packaging.specifiers import SpecifierSet, InvalidSpecifier from src.common.logger_manager import get_logger +from rich.traceback import install +install(show_locals=True, extra_lines=3) # 配置主程序日志格式 diff --git a/src/do_tool/tool_can_use/base_tool.py b/src/do_tool/tool_can_use/base_tool.py index 42570884..298bc15a 100644 --- a/src/do_tool/tool_can_use/base_tool.py +++ b/src/do_tool/tool_can_use/base_tool.py @@ -4,6 +4,8 @@ import importlib import pkgutil import os from src.common.logger_manager import get_logger +from rich.traceback import install +install(show_locals=True, extra_lines=3) logger = get_logger("base_tool") diff --git a/src/individuality/individuality.py b/src/individuality/individuality.py index 9ffdfdaa..6befe3e5 100644 --- a/src/individuality/individuality.py +++ b/src/individuality/individuality.py @@ -2,6 +2,8 @@ from typing import Optional from .personality import Personality from .identity import Identity import random +from rich.traceback import install +install(show_locals=True, extra_lines=3) class Individuality: diff --git a/src/individuality/offline_llm.py b/src/individuality/offline_llm.py index 2b5b6dc2..2059d7fd 100644 --- a/src/individuality/offline_llm.py +++ b/src/individuality/offline_llm.py @@ -6,6 +6,8 @@ from typing import Tuple, Union import aiohttp import requests from src.common.logger import get_module_logger +from rich.traceback import install +install(show_locals=True, extra_lines=3) logger = get_module_logger("offline_llm") diff --git a/src/main.py b/src/main.py index c0e743d6..1f6ed188 100644 --- a/src/main.py +++ b/src/main.py @@ -17,6 +17,8 @@ from .common.logger_manager import get_logger from .plugins.remote import heartbeat_thread # noqa: F401 from .individuality.individuality import Individuality from .common.server import global_server +from rich.traceback import install +install(show_locals=True, extra_lines=3) logger = get_logger("main") diff --git a/src/plugins/PFC/chat_observer.py b/src/plugins/PFC/chat_observer.py index 1a17db90..675e9963 100644 --- a/src/plugins/PFC/chat_observer.py +++ b/src/plugins/PFC/chat_observer.py @@ -7,6 +7,8 @@ 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 +from rich.traceback import install +install(show_locals=True, extra_lines=3) logger = get_module_logger("chat_observer") diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 9f744c30..f7702e1b 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -23,6 +23,8 @@ from .pfc_KnowledgeFetcher import KnowledgeFetcher from .waiter import Waiter import traceback +from rich.traceback import install +install(show_locals=True, extra_lines=3) logger = get_logger("pfc") diff --git a/src/plugins/PFC/message_sender.py b/src/plugins/PFC/message_sender.py index 53c20374..709150c7 100644 --- a/src/plugins/PFC/message_sender.py +++ b/src/plugins/PFC/message_sender.py @@ -8,6 +8,8 @@ from src.plugins.chat.message import MessageSending, MessageSet from src.plugins.chat.message_sender import message_manager from ..storage.storage import MessageStorage from ...config.config import global_config +from rich.traceback import install +install(show_locals=True, extra_lines=3) logger = get_module_logger("message_sender") diff --git a/src/plugins/PFC/pfc.py b/src/plugins/PFC/pfc.py index 6cb1fe83..4ce98a52 100644 --- a/src/plugins/PFC/pfc.py +++ b/src/plugins/PFC/pfc.py @@ -8,6 +8,8 @@ from src.individuality.individuality import Individuality from .conversation_info import ConversationInfo from .observation_info import ObservationInfo from src.plugins.utils.chat_message_builder import build_readable_messages +from rich.traceback import install +install(show_locals=True, extra_lines=3) if TYPE_CHECKING: pass diff --git a/src/plugins/chat/chat_stream.py b/src/plugins/chat/chat_stream.py index 14d02a81..d3095f3b 100644 --- a/src/plugins/chat/chat_stream.py +++ b/src/plugins/chat/chat_stream.py @@ -9,6 +9,8 @@ from ...common.database import db from maim_message import GroupInfo, UserInfo from src.common.logger_manager import get_logger +from rich.traceback import install +install(show_locals=True, extra_lines=3) logger = get_logger("chat_stream") diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py index a0ff33ab..4c11dd98 100644 --- a/src/plugins/chat/message.py +++ b/src/plugins/chat/message.py @@ -9,6 +9,8 @@ from src.common.logger_manager import get_logger from .chat_stream import ChatStream from .utils_image import image_manager from maim_message import Seg, UserInfo, BaseMessageInfo, MessageBase +from rich.traceback import install +install(show_locals=True, extra_lines=3) logger = get_logger("chat_message") diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index 61e2dd49..aaa48ca8 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -13,6 +13,8 @@ from ...config.config import global_config from .utils import truncate_message, calculate_typing_time, count_messages_between from src.common.logger_manager import get_logger +from rich.traceback import install +install(show_locals=True, extra_lines=3) logger = get_logger("sender") diff --git a/src/plugins/chat/utils_image.py b/src/plugins/chat/utils_image.py index f567c527..285b6394 100644 --- a/src/plugins/chat/utils_image.py +++ b/src/plugins/chat/utils_image.py @@ -13,6 +13,8 @@ from ...config.config import global_config from ..models.utils_model import LLMRequest from src.common.logger_manager import get_logger +from rich.traceback import install +install(show_locals=True, extra_lines=3) logger = get_logger("chat_image") diff --git a/src/plugins/config_reload/api.py b/src/plugins/config_reload/api.py index 327451e2..93b5d1cb 100644 --- a/src/plugins/config_reload/api.py +++ b/src/plugins/config_reload/api.py @@ -1,4 +1,6 @@ from fastapi import APIRouter, HTTPException +from rich.traceback import install +install(show_locals=True, extra_lines=3) # 创建APIRouter而不是FastAPI实例 router = APIRouter() diff --git a/src/plugins/emoji_system/emoji_manager.py b/src/plugins/emoji_system/emoji_manager.py index f789f906..67075e29 100644 --- a/src/plugins/emoji_system/emoji_manager.py +++ b/src/plugins/emoji_system/emoji_manager.py @@ -15,6 +15,8 @@ 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_manager import get_logger +from rich.traceback import install +install(show_locals=True, extra_lines=3) logger = get_logger("emoji") diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index f203926f..077d623b 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -27,6 +27,8 @@ from src.plugins.chat.utils import process_llm_response from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager from src.plugins.moods.moods import MoodManager from src.heart_flow.utils_chat import get_chat_type_and_target_info +from rich.traceback import install +install(show_locals=True, extra_lines=3) WAITING_TIME_THRESHOLD = 300 # 等待新消息时间阈值,单位秒 diff --git a/src/plugins/heartFC_chat/heartFC_sender.py b/src/plugins/heartFC_chat/heartFC_sender.py index ee4e86ee..001f578a 100644 --- a/src/plugins/heartFC_chat/heartFC_sender.py +++ b/src/plugins/heartFC_chat/heartFC_sender.py @@ -9,6 +9,8 @@ from ..storage.storage import MessageStorage from ..chat.utils import truncate_message from src.common.logger_manager import get_logger from src.plugins.chat.utils import calculate_typing_time +from rich.traceback import install +install(show_locals=True, extra_lines=3) logger = get_logger("sender") diff --git a/src/plugins/knowledge/src/embedding_store.py b/src/plugins/knowledge/src/embedding_store.py index 9e60b8e1..98ab757c 100644 --- a/src/plugins/knowledge/src/embedding_store.py +++ b/src/plugins/knowledge/src/embedding_store.py @@ -12,6 +12,8 @@ from .llm_client import LLMClient from .lpmmconfig import ENT_NAMESPACE, PG_NAMESPACE, REL_NAMESPACE, global_config from .utils.hash import get_sha256 from .global_logger import logger +from rich.traceback import install +install(show_locals=True, extra_lines=3) @dataclass diff --git a/src/plugins/memory_system/Hippocampus.py b/src/plugins/memory_system/Hippocampus.py index ccc4d4fa..789da590 100644 --- a/src/plugins/memory_system/Hippocampus.py +++ b/src/plugins/memory_system/Hippocampus.py @@ -20,6 +20,8 @@ from ..utils.chat_message_builder import ( ) # 导入 build_readable_messages from ..chat.utils import translate_timestamp_to_human_readable from .memory_config import MemoryConfig +from rich.traceback import install +install(show_locals=True, extra_lines=3) def calculate_information_content(text): diff --git a/src/plugins/memory_system/debug_memory.py b/src/plugins/memory_system/debug_memory.py index 4e357557..03c8fcac 100644 --- a/src/plugins/memory_system/debug_memory.py +++ b/src/plugins/memory_system/debug_memory.py @@ -8,6 +8,8 @@ import os sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))) from src.plugins.memory_system.Hippocampus import HippocampusManager from src.config.config import global_config +from rich.traceback import install +install(show_locals=True, extra_lines=3) async def test_memory_system(): diff --git a/src/plugins/memory_system/manually_alter_memory.py b/src/plugins/memory_system/manually_alter_memory.py index 1452d3d5..4054265b 100644 --- a/src/plugins/memory_system/manually_alter_memory.py +++ b/src/plugins/memory_system/manually_alter_memory.py @@ -9,6 +9,8 @@ from Hippocampus import Hippocampus # 海马体和记忆图 from dotenv import load_dotenv +from rich.traceback import install +install(show_locals=True, extra_lines=3) """ diff --git a/src/plugins/memory_system/offline_llm.py b/src/plugins/memory_system/offline_llm.py index fc50b17b..f86ec411 100644 --- a/src/plugins/memory_system/offline_llm.py +++ b/src/plugins/memory_system/offline_llm.py @@ -6,6 +6,8 @@ from typing import Tuple, Union import aiohttp import requests from src.common.logger import get_module_logger +from rich.traceback import install +install(show_locals=True, extra_lines=3) logger = get_module_logger("offline_llm") diff --git a/src/plugins/memory_system/sample_distribution.py b/src/plugins/memory_system/sample_distribution.py index 5dae2f26..f4fec12b 100644 --- a/src/plugins/memory_system/sample_distribution.py +++ b/src/plugins/memory_system/sample_distribution.py @@ -1,6 +1,8 @@ import numpy as np from scipy import stats from datetime import datetime, timedelta +from rich.traceback import install +install(show_locals=True, extra_lines=3) class DistributionVisualizer: diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py index 6b9c0416..60b507b9 100644 --- a/src/plugins/models/utils_model.py +++ b/src/plugins/models/utils_model.py @@ -14,6 +14,8 @@ import io import os from ...common.database import db from ...config.config import global_config +from rich.traceback import install +install(show_locals=True, extra_lines=3) logger = get_module_logger("model_utils") diff --git a/src/plugins/utils/prompt_builder.py b/src/plugins/utils/prompt_builder.py index 578d9677..02347033 100644 --- a/src/plugins/utils/prompt_builder.py +++ b/src/plugins/utils/prompt_builder.py @@ -4,6 +4,8 @@ from contextlib import asynccontextmanager import asyncio from src.common.logger import get_module_logger # import traceback +from rich.traceback import install +install(show_locals=True, extra_lines=3) logger = get_module_logger("prompt_build") diff --git a/src/plugins/utils/timer_calculator.py b/src/plugins/utils/timer_calculator.py index 13bc26f1..076a4fe5 100644 --- a/src/plugins/utils/timer_calculator.py +++ b/src/plugins/utils/timer_calculator.py @@ -2,6 +2,8 @@ from time import perf_counter from functools import wraps from typing import Optional, Dict, Callable import asyncio +from rich.traceback import install +install(show_locals=True, extra_lines=3) """ # 更好的计时器 diff --git a/src/plugins/willing/willing_manager.py b/src/plugins/willing/willing_manager.py index c26325b1..dc4abfae 100644 --- a/src/plugins/willing/willing_manager.py +++ b/src/plugins/willing/willing_manager.py @@ -8,6 +8,8 @@ from abc import ABC, abstractmethod import importlib from typing import Dict, Optional import asyncio +from rich.traceback import install +install(show_locals=True, extra_lines=3) """ 基类方法概览: diff --git a/src/plugins/zhishi/knowledge_library.py b/src/plugins/zhishi/knowledge_library.py index f8914c2f..1a8d35a3 100644 --- a/src/plugins/zhishi/knowledge_library.py +++ b/src/plugins/zhishi/knowledge_library.py @@ -7,6 +7,8 @@ from datetime import datetime from tqdm import tqdm from rich.console import Console from rich.table import Table +from rich.traceback import install +install(show_locals=True, extra_lines=3) # 添加项目根目录到 Python 路径 root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) @@ -15,6 +17,7 @@ sys.path.append(root_path) # 现在可以导入src模块 from src.common.database import db # noqa E402 + # 加载根目录下的env.edv文件 env_path = os.path.join(root_path, ".env") if not os.path.exists(env_path): From c88a73597a6c07fab73cf5d49f4cc84b2b54e21e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 1 May 2025 19:18:37 +0000 Subject: [PATCH 41/67] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 1 + src/common/database.py | 1 + src/common/log_decorators.py | 1 + src/common/server.py | 1 + src/config/config.py | 1 + src/do_tool/tool_can_use/base_tool.py | 1 + src/individuality/individuality.py | 1 + src/individuality/offline_llm.py | 1 + src/main.py | 1 + src/plugins/PFC/chat_observer.py | 1 + src/plugins/PFC/conversation.py | 1 + src/plugins/PFC/message_sender.py | 1 + src/plugins/PFC/pfc.py | 1 + src/plugins/chat/chat_stream.py | 1 + src/plugins/chat/message.py | 1 + src/plugins/chat/message_sender.py | 1 + src/plugins/chat/utils_image.py | 1 + src/plugins/config_reload/api.py | 1 + src/plugins/emoji_system/emoji_manager.py | 1 + src/plugins/heartFC_chat/heartFC_chat.py | 1 + src/plugins/heartFC_chat/heartFC_sender.py | 1 + src/plugins/knowledge/src/embedding_store.py | 1 + src/plugins/memory_system/Hippocampus.py | 1 + src/plugins/memory_system/debug_memory.py | 1 + src/plugins/memory_system/manually_alter_memory.py | 1 + src/plugins/memory_system/offline_llm.py | 1 + src/plugins/memory_system/sample_distribution.py | 1 + src/plugins/models/utils_model.py | 1 + src/plugins/utils/prompt_builder.py | 2 ++ src/plugins/utils/timer_calculator.py | 1 + src/plugins/willing/willing_manager.py | 1 + src/plugins/zhishi/knowledge_library.py | 1 + 32 files changed, 33 insertions(+) diff --git a/bot.py b/bot.py index ecdbd2b9..5d811d4e 100644 --- a/bot.py +++ b/bot.py @@ -14,6 +14,7 @@ from src.common.logger_manager import get_logger from src.common.crash_logger import install_crash_handler from src.main import MainSystem from rich.traceback import install + install(show_locals=True, extra_lines=3) diff --git a/src/common/database.py b/src/common/database.py index 846c4ee7..66a2dc16 100644 --- a/src/common/database.py +++ b/src/common/database.py @@ -2,6 +2,7 @@ import os from pymongo import MongoClient from pymongo.database import Database from rich.traceback import install + install(show_locals=True, extra_lines=3) _client = None diff --git a/src/common/log_decorators.py b/src/common/log_decorators.py index 4e881984..a57fae79 100644 --- a/src/common/log_decorators.py +++ b/src/common/log_decorators.py @@ -3,6 +3,7 @@ import inspect from typing import Callable, Any from .logger import logger, add_custom_style_handler from rich.traceback import install + install(show_locals=True, extra_lines=3) diff --git a/src/common/server.py b/src/common/server.py index 0511f443..c080e28a 100644 --- a/src/common/server.py +++ b/src/common/server.py @@ -3,6 +3,7 @@ from typing import Optional from uvicorn import Config, Server as UvicornServer import os from rich.traceback import install + install(show_locals=True, extra_lines=3) diff --git a/src/config/config.py b/src/config/config.py index 29c28c83..a067633b 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -15,6 +15,7 @@ from packaging.specifiers import SpecifierSet, InvalidSpecifier from src.common.logger_manager import get_logger from rich.traceback import install + install(show_locals=True, extra_lines=3) diff --git a/src/do_tool/tool_can_use/base_tool.py b/src/do_tool/tool_can_use/base_tool.py index 298bc15a..88da036d 100644 --- a/src/do_tool/tool_can_use/base_tool.py +++ b/src/do_tool/tool_can_use/base_tool.py @@ -5,6 +5,7 @@ import pkgutil import os from src.common.logger_manager import get_logger from rich.traceback import install + install(show_locals=True, extra_lines=3) logger = get_logger("base_tool") diff --git a/src/individuality/individuality.py b/src/individuality/individuality.py index 6befe3e5..963fae0e 100644 --- a/src/individuality/individuality.py +++ b/src/individuality/individuality.py @@ -3,6 +3,7 @@ from .personality import Personality from .identity import Identity import random from rich.traceback import install + install(show_locals=True, extra_lines=3) diff --git a/src/individuality/offline_llm.py b/src/individuality/offline_llm.py index 2059d7fd..0e1a446c 100644 --- a/src/individuality/offline_llm.py +++ b/src/individuality/offline_llm.py @@ -7,6 +7,7 @@ import aiohttp import requests from src.common.logger import get_module_logger from rich.traceback import install + install(show_locals=True, extra_lines=3) logger = get_module_logger("offline_llm") diff --git a/src/main.py b/src/main.py index 1f6ed188..3de3e880 100644 --- a/src/main.py +++ b/src/main.py @@ -18,6 +18,7 @@ from .plugins.remote import heartbeat_thread # noqa: F401 from .individuality.individuality import Individuality from .common.server import global_server from rich.traceback import install + install(show_locals=True, extra_lines=3) logger = get_logger("main") diff --git a/src/plugins/PFC/chat_observer.py b/src/plugins/PFC/chat_observer.py index 675e9963..34b66316 100644 --- a/src/plugins/PFC/chat_observer.py +++ b/src/plugins/PFC/chat_observer.py @@ -8,6 +8,7 @@ from ...config.config import global_config from .chat_states import NotificationManager, create_new_message_notification, create_cold_chat_notification from .message_storage import MongoDBMessageStorage from rich.traceback import install + install(show_locals=True, extra_lines=3) logger = get_module_logger("chat_observer") diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index f7702e1b..925fd7b5 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -24,6 +24,7 @@ from .waiter import Waiter import traceback from rich.traceback import install + install(show_locals=True, extra_lines=3) logger = get_logger("pfc") diff --git a/src/plugins/PFC/message_sender.py b/src/plugins/PFC/message_sender.py index 709150c7..f1085768 100644 --- a/src/plugins/PFC/message_sender.py +++ b/src/plugins/PFC/message_sender.py @@ -9,6 +9,7 @@ from src.plugins.chat.message_sender import message_manager from ..storage.storage import MessageStorage from ...config.config import global_config from rich.traceback import install + install(show_locals=True, extra_lines=3) diff --git a/src/plugins/PFC/pfc.py b/src/plugins/PFC/pfc.py index 4ce98a52..50f7bf4c 100644 --- a/src/plugins/PFC/pfc.py +++ b/src/plugins/PFC/pfc.py @@ -9,6 +9,7 @@ from .conversation_info import ConversationInfo from .observation_info import ObservationInfo from src.plugins.utils.chat_message_builder import build_readable_messages from rich.traceback import install + install(show_locals=True, extra_lines=3) if TYPE_CHECKING: diff --git a/src/plugins/chat/chat_stream.py b/src/plugins/chat/chat_stream.py index d3095f3b..a949247c 100644 --- a/src/plugins/chat/chat_stream.py +++ b/src/plugins/chat/chat_stream.py @@ -10,6 +10,7 @@ from maim_message import GroupInfo, UserInfo from src.common.logger_manager import get_logger from rich.traceback import install + install(show_locals=True, extra_lines=3) diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py index 4c11dd98..354082e1 100644 --- a/src/plugins/chat/message.py +++ b/src/plugins/chat/message.py @@ -10,6 +10,7 @@ from .chat_stream import ChatStream from .utils_image import image_manager from maim_message import Seg, UserInfo, BaseMessageInfo, MessageBase from rich.traceback import install + install(show_locals=True, extra_lines=3) logger = get_logger("chat_message") diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index aaa48ca8..8bfee44b 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -14,6 +14,7 @@ from .utils import truncate_message, calculate_typing_time, count_messages_betwe from src.common.logger_manager import get_logger from rich.traceback import install + install(show_locals=True, extra_lines=3) diff --git a/src/plugins/chat/utils_image.py b/src/plugins/chat/utils_image.py index 285b6394..1f734502 100644 --- a/src/plugins/chat/utils_image.py +++ b/src/plugins/chat/utils_image.py @@ -14,6 +14,7 @@ from ..models.utils_model import LLMRequest from src.common.logger_manager import get_logger from rich.traceback import install + install(show_locals=True, extra_lines=3) logger = get_logger("chat_image") diff --git a/src/plugins/config_reload/api.py b/src/plugins/config_reload/api.py index 93b5d1cb..ee0a5454 100644 --- a/src/plugins/config_reload/api.py +++ b/src/plugins/config_reload/api.py @@ -1,5 +1,6 @@ from fastapi import APIRouter, HTTPException from rich.traceback import install + install(show_locals=True, extra_lines=3) # 创建APIRouter而不是FastAPI实例 diff --git a/src/plugins/emoji_system/emoji_manager.py b/src/plugins/emoji_system/emoji_manager.py index 67075e29..24266c08 100644 --- a/src/plugins/emoji_system/emoji_manager.py +++ b/src/plugins/emoji_system/emoji_manager.py @@ -16,6 +16,7 @@ from ..chat.utils_image import image_path_to_base64, image_manager from ..models.utils_model import LLMRequest from src.common.logger_manager import get_logger from rich.traceback import install + install(show_locals=True, extra_lines=3) logger = get_logger("emoji") diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 077d623b..712b6af5 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -28,6 +28,7 @@ from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager from src.plugins.moods.moods import MoodManager from src.heart_flow.utils_chat import get_chat_type_and_target_info from rich.traceback import install + install(show_locals=True, extra_lines=3) diff --git a/src/plugins/heartFC_chat/heartFC_sender.py b/src/plugins/heartFC_chat/heartFC_sender.py index 001f578a..6fab5d62 100644 --- a/src/plugins/heartFC_chat/heartFC_sender.py +++ b/src/plugins/heartFC_chat/heartFC_sender.py @@ -10,6 +10,7 @@ from ..chat.utils import truncate_message from src.common.logger_manager import get_logger from src.plugins.chat.utils import calculate_typing_time from rich.traceback import install + install(show_locals=True, extra_lines=3) diff --git a/src/plugins/knowledge/src/embedding_store.py b/src/plugins/knowledge/src/embedding_store.py index 98ab757c..72c6c7b5 100644 --- a/src/plugins/knowledge/src/embedding_store.py +++ b/src/plugins/knowledge/src/embedding_store.py @@ -13,6 +13,7 @@ from .lpmmconfig import ENT_NAMESPACE, PG_NAMESPACE, REL_NAMESPACE, global_confi from .utils.hash import get_sha256 from .global_logger import logger from rich.traceback import install + install(show_locals=True, extra_lines=3) diff --git a/src/plugins/memory_system/Hippocampus.py b/src/plugins/memory_system/Hippocampus.py index 789da590..11ba8f40 100644 --- a/src/plugins/memory_system/Hippocampus.py +++ b/src/plugins/memory_system/Hippocampus.py @@ -21,6 +21,7 @@ from ..utils.chat_message_builder import ( from ..chat.utils import translate_timestamp_to_human_readable from .memory_config import MemoryConfig from rich.traceback import install + install(show_locals=True, extra_lines=3) diff --git a/src/plugins/memory_system/debug_memory.py b/src/plugins/memory_system/debug_memory.py index 03c8fcac..ae767c85 100644 --- a/src/plugins/memory_system/debug_memory.py +++ b/src/plugins/memory_system/debug_memory.py @@ -9,6 +9,7 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname( from src.plugins.memory_system.Hippocampus import HippocampusManager from src.config.config import global_config from rich.traceback import install + install(show_locals=True, extra_lines=3) diff --git a/src/plugins/memory_system/manually_alter_memory.py b/src/plugins/memory_system/manually_alter_memory.py index 4054265b..10a75738 100644 --- a/src/plugins/memory_system/manually_alter_memory.py +++ b/src/plugins/memory_system/manually_alter_memory.py @@ -10,6 +10,7 @@ from Hippocampus import Hippocampus # 海马体和记忆图 from dotenv import load_dotenv from rich.traceback import install + install(show_locals=True, extra_lines=3) diff --git a/src/plugins/memory_system/offline_llm.py b/src/plugins/memory_system/offline_llm.py index f86ec411..335a76d3 100644 --- a/src/plugins/memory_system/offline_llm.py +++ b/src/plugins/memory_system/offline_llm.py @@ -7,6 +7,7 @@ import aiohttp import requests from src.common.logger import get_module_logger from rich.traceback import install + install(show_locals=True, extra_lines=3) logger = get_module_logger("offline_llm") diff --git a/src/plugins/memory_system/sample_distribution.py b/src/plugins/memory_system/sample_distribution.py index f4fec12b..76796728 100644 --- a/src/plugins/memory_system/sample_distribution.py +++ b/src/plugins/memory_system/sample_distribution.py @@ -2,6 +2,7 @@ import numpy as np from scipy import stats from datetime import datetime, timedelta from rich.traceback import install + install(show_locals=True, extra_lines=3) diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py index 60b507b9..7c7fe713 100644 --- a/src/plugins/models/utils_model.py +++ b/src/plugins/models/utils_model.py @@ -15,6 +15,7 @@ import os from ...common.database import db from ...config.config import global_config from rich.traceback import install + install(show_locals=True, extra_lines=3) logger = get_module_logger("model_utils") diff --git a/src/plugins/utils/prompt_builder.py b/src/plugins/utils/prompt_builder.py index 02347033..c4555a55 100644 --- a/src/plugins/utils/prompt_builder.py +++ b/src/plugins/utils/prompt_builder.py @@ -3,8 +3,10 @@ import re from contextlib import asynccontextmanager import asyncio from src.common.logger import get_module_logger + # import traceback from rich.traceback import install + install(show_locals=True, extra_lines=3) logger = get_module_logger("prompt_build") diff --git a/src/plugins/utils/timer_calculator.py b/src/plugins/utils/timer_calculator.py index 076a4fe5..d66f21cc 100644 --- a/src/plugins/utils/timer_calculator.py +++ b/src/plugins/utils/timer_calculator.py @@ -3,6 +3,7 @@ from functools import wraps from typing import Optional, Dict, Callable import asyncio from rich.traceback import install + install(show_locals=True, extra_lines=3) """ diff --git a/src/plugins/willing/willing_manager.py b/src/plugins/willing/willing_manager.py index dc4abfae..a5884da2 100644 --- a/src/plugins/willing/willing_manager.py +++ b/src/plugins/willing/willing_manager.py @@ -9,6 +9,7 @@ import importlib from typing import Dict, Optional import asyncio from rich.traceback import install + install(show_locals=True, extra_lines=3) """ diff --git a/src/plugins/zhishi/knowledge_library.py b/src/plugins/zhishi/knowledge_library.py index 1a8d35a3..26af3bda 100644 --- a/src/plugins/zhishi/knowledge_library.py +++ b/src/plugins/zhishi/knowledge_library.py @@ -8,6 +8,7 @@ from tqdm import tqdm from rich.console import Console from rich.table import Table from rich.traceback import install + install(show_locals=True, extra_lines=3) # 添加项目根目录到 Python 路径 From 80747000b2c1c8bf5753c89212bc4a66c3638449 Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Fri, 2 May 2025 11:34:24 +0800 Subject: [PATCH 42/67] fix --- src/plugins/PFC/action_planner.py | 75 +++++++++++++++++++----------- src/plugins/PFC/conversation.py | 44 +++++++++--------- src/plugins/PFC/reply_generator.py | 56 +++++++++++----------- 3 files changed, 99 insertions(+), 76 deletions(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index a86ddf0a..1f10c973 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -360,38 +360,38 @@ class ActionPlanner: goals_str = "- 构建对话目标时出错。\n" # --- 知识信息字符串构建开始 --- - knowledge_info_str = "【已获取的相关知识和记忆】\n" - try: + # knowledge_info_str = "【已获取的相关知识和记忆】\n" + # try: # 检查 conversation_info 是否有 knowledge_list 并且不为空 - if hasattr(conversation_info, "knowledge_list") and conversation_info.knowledge_list: + # if hasattr(conversation_info, "knowledge_list") and conversation_info.knowledge_list: # 最多只显示最近的 5 条知识,防止 Prompt 过长 - recent_knowledge = conversation_info.knowledge_list[-5:] - for i, knowledge_item in enumerate(recent_knowledge): - if isinstance(knowledge_item, dict): - query = knowledge_item.get("query", "未知查询") - knowledge = knowledge_item.get("knowledge", "无知识内容") - source = knowledge_item.get("source", "未知来源") + # recent_knowledge = conversation_info.knowledge_list[-5:] + # for i, knowledge_item in enumerate(recent_knowledge): + # if isinstance(knowledge_item, dict): + # query = knowledge_item.get("query", "未知查询") + # knowledge = knowledge_item.get("knowledge", "无知识内容") + # source = knowledge_item.get("source", "未知来源") # 只取知识内容的前 2000 个字,避免太长 - knowledge_snippet = knowledge[:2000] + "..." if len(knowledge) > 2000 else knowledge - knowledge_info_str += ( - f"{i + 1}. 关于 '{query}' 的知识 (来源: {source}):\n {knowledge_snippet}\n" - ) - else: + # knowledge_snippet = knowledge[:2000] + "..." if len(knowledge) > 2000 else knowledge + # knowledge_info_str += ( + # f"{i + 1}. 关于 '{query}' 的知识 (来源: {source}):\n {knowledge_snippet}\n" + # ) + # else: # 处理列表里不是字典的异常情况 - knowledge_info_str += f"{i + 1}. 发现一条格式不正确的知识记录。\n" + # knowledge_info_str += f"{i + 1}. 发现一条格式不正确的知识记录。\n" - if not recent_knowledge: # 如果 knowledge_list 存在但为空 - knowledge_info_str += "- 暂无相关知识和记忆。\n" + # if not recent_knowledge: # 如果 knowledge_list 存在但为空 + # knowledge_info_str += "- 暂无相关知识和记忆。\n" - else: + # else: # 如果 conversation_info 没有 knowledge_list 属性,或者列表为空 - knowledge_info_str += "- 暂无相关知识记忆。\n" - except AttributeError: - logger.warning(f"[私聊][{self.private_name}]ConversationInfo 对象可能缺少 knowledge_list 属性。") - knowledge_info_str += "- 获取知识列表时出错。\n" - except Exception as e: - logger.error(f"[私聊][{self.private_name}]构建知识信息字符串时出错: {e}") - knowledge_info_str += "- 处理知识列表时出错。\n" + # knowledge_info_str += "- 暂无相关知识记忆。\n" + # except AttributeError: + # logger.warning(f"[私聊][{self.private_name}]ConversationInfo 对象可能缺少 knowledge_list 属性。") + # knowledge_info_str += "- 获取知识列表时出错。\n" + # except Exception as e: + # logger.error(f"[私聊][{self.private_name}]构建知识信息字符串时出错: {e}") + # knowledge_info_str += "- 处理知识列表时出错。\n" # --- 知识信息字符串构建结束 --- # 获取聊天历史记录 (chat_history_text) @@ -501,6 +501,27 @@ class ActionPlanner: last_action_context += f"- 该行动当前状态: {status}\n" # self.last_successful_action_type = None # 非完成状态,清除记录 + retrieved_memory_str_planner = "" + retrieved_knowledge_str_planner = "" + retrieval_context = chat_history_text # 使用聊天记录作为检索上下文 + if retrieval_context and retrieval_context != "还没有聊天记录。" and retrieval_context != "[构建聊天记录出错]": + try: + logger.debug(f"[私聊][{self.private_name}] (ActionPlanner) 开始自动检索记忆...") + retrieved_memory_str_planner = await self._get_memory_info(text=retrieval_context) + logger.info(f"[私聊][{self.private_name}] (ActionPlanner) 自动检索记忆 {'完成' if retrieved_memory_str_planner else '无结果'}。") + + logger.debug(f"[私聊][{self.private_name}] (ActionPlanner) 开始自动知识检索...") + retrieved_knowledge_str_planner = await self._get_prompt_info(message=retrieval_context) + logger.info(f"[私聊][{self.private_name}] (ActionPlanner) 自动检索知识 {'完成' if retrieved_knowledge_str_planner else '无结果'}。") + except Exception as retrieval_err: + logger.error(f"[私聊][{self.private_name}] (ActionPlanner) 自动检索时出错: {retrieval_err}") + retrieved_memory_str_planner = "检索记忆时出错。\n" + retrieved_knowledge_str_planner = "检索知识时出错。\n" + else: + logger.debug(f"[私聊][{self.private_name}] (ActionPlanner) 无有效聊天记录,跳过自动检索。") + retrieved_memory_str_planner = "无聊天记录无法检索记忆。\n" + retrieved_knowledge_str_planner = "无聊天记录无法检索知识。\n" + # --- 选择 Prompt --- if last_successful_reply_action in ["direct_reply", "send_new_message"]: prompt_template = PROMPT_FOLLOW_UP @@ -518,7 +539,9 @@ class ActionPlanner: time_since_last_bot_message_info=time_since_last_bot_message_info, timeout_context=timeout_context, chat_history_text=chat_history_text if chat_history_text.strip() else "还没有聊天记录。", - knowledge_info_str=knowledge_info_str, + # knowledge_info_str=knowledge_info_str, + retrieved_memory_str=retrieved_memory_str_planner if retrieved_memory_str_planner else "无相关记忆。", + retrieved_knowledge_str=retrieved_knowledge_str_planner if retrieved_knowledge_str_planner else "无相关知识。" ) logger.debug(f"[私聊][{self.private_name}]发送到LLM的最终提示词:\n------\n{prompt}\n------") diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 9f744c30..2ecd6824 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -505,31 +505,31 @@ class Conversation: } conversation_info.done_action.append(wait_action_record) - elif action == "fetch_knowledge": - self.state = ConversationState.FETCHING - knowledge_query = reason - try: + # elif action == "fetch_knowledge": + # self.state = ConversationState.FETCHING + # knowledge_query = reason + # try: # 检查 knowledge_fetcher 是否存在 - if not hasattr(self, "knowledge_fetcher"): - logger.error(f"[私聊][{self.private_name}]KnowledgeFetcher 未初始化,无法获取知识。") - raise AttributeError("KnowledgeFetcher not initialized") + # if not hasattr(self, "knowledge_fetcher"): + # logger.error(f"[私聊][{self.private_name}]KnowledgeFetcher 未初始化,无法获取知识。") + # raise AttributeError("KnowledgeFetcher not initialized") - knowledge, source = await self.knowledge_fetcher.fetch(knowledge_query, observation_info.chat_history) - logger.info(f"[私聊][{self.private_name}]获取到知识: {knowledge[:100]}..., 来源: {source}") - if knowledge: + # knowledge, source = await self.knowledge_fetcher.fetch(knowledge_query, observation_info.chat_history) + # logger.info(f"[私聊][{self.private_name}]获取到知识: {knowledge[:100]}..., 来源: {source}") + # if knowledge: # 确保 knowledge_list 存在 - if not hasattr(conversation_info, "knowledge_list"): - conversation_info.knowledge_list = [] - conversation_info.knowledge_list.append( - {"query": knowledge_query, "knowledge": knowledge, "source": source} - ) - action_successful = True - except Exception as fetch_err: - logger.error(f"[私聊][{self.private_name}]获取知识时出错: {str(fetch_err)}") - conversation_info.done_action[action_index].update( - {"status": "recall", "final_reason": f"获取知识失败: {str(fetch_err)}"} - ) - self.conversation_info.last_successful_reply_action = None # 重置状态 + # if not hasattr(conversation_info, "knowledge_list"): + # conversation_info.knowledge_list = [] + # conversation_info.knowledge_list.append( + # {"query": knowledge_query, "knowledge": knowledge, "source": source} + # ) + # action_successful = True + # except Exception as fetch_err: + # logger.error(f"[私聊][{self.private_name}]获取知识时出错: {str(fetch_err)}") + # conversation_info.done_action[action_index].update( + # {"status": "recall", "final_reason": f"获取知识失败: {str(fetch_err)}"} + # ) + # self.conversation_info.last_successful_reply_action = None # 重置状态 elif action == "rethink_goal": self.state = ConversationState.RETHINKING diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index 1b2362f3..a9ed61fd 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -37,7 +37,7 @@ PROMPT_DIRECT_REPLY = """{persona_text}。现在你在参与一场QQ私聊,请 最近的聊天记录: {chat_history_text} -{related_memory_info}。 +{retrieved_memory_str} 请根据上述信息,结合聊天记录,回复对方。该回复应该: @@ -66,7 +66,7 @@ PROMPT_SEND_NEW_MESSAGE = """{persona_text}。现在你在参与一场QQ私聊 最近的聊天记录: {chat_history_text} -{related_memory_info} +{retrieved_memory_str} 请根据上述信息,结合聊天记录,继续发一条新消息(例如对之前消息的补充,深入话题,或追问等等)。该消息应该: 1. 符合对话目标,以"你"的角度发言(不要自己与自己对话!) @@ -282,36 +282,36 @@ class ReplyGenerator: goals_str = "- 目前没有明确对话目标\n" # 简化无目标情况 # --- 新增:构建知识信息字符串 --- - knowledge_info_str = "【供参考的相关知识和记忆】\n" # 稍微改下标题,表明是供参考 - try: + # knowledge_info_str = "【供参考的相关知识和记忆】\n" # 稍微改下标题,表明是供参考 + # try: # 检查 conversation_info 是否有 knowledge_list 并且不为空 - if hasattr(conversation_info, "knowledge_list") and conversation_info.knowledge_list: + # if hasattr(conversation_info, "knowledge_list") and conversation_info.knowledge_list: # 最多只显示最近的 5 条知识 - recent_knowledge = conversation_info.knowledge_list[-5:] - for i, knowledge_item in enumerate(recent_knowledge): - if isinstance(knowledge_item, dict): - query = knowledge_item.get("query", "未知查询") - knowledge = knowledge_item.get("knowledge", "无知识内容") - source = knowledge_item.get("source", "未知来源") + # recent_knowledge = conversation_info.knowledge_list[-5:] + # for i, knowledge_item in enumerate(recent_knowledge): + # if isinstance(knowledge_item, dict): + # query = knowledge_item.get("query", "未知查询") + # knowledge = knowledge_item.get("knowledge", "无知识内容") + # source = knowledge_item.get("source", "未知来源") # 只取知识内容的前 2000 个字 - knowledge_snippet = knowledge[:2000] + "..." if len(knowledge) > 2000 else knowledge - knowledge_info_str += ( - f"{i + 1}. 关于 '{query}' (来源: {source}): {knowledge_snippet}\n" # 格式微调,更简洁 - ) - else: - knowledge_info_str += f"{i + 1}. 发现一条格式不正确的知识记录。\n" + # knowledge_snippet = knowledge[:2000] + "..." if len(knowledge) > 2000 else knowledge + # knowledge_info_str += ( + # f"{i + 1}. 关于 '{query}' (来源: {source}): {knowledge_snippet}\n" # 格式微调,更简洁 + # ) + # else: + # knowledge_info_str += f"{i + 1}. 发现一条格式不正确的知识记录。\n" - if not recent_knowledge: - knowledge_info_str += "- 暂无。\n" # 更简洁的提示 + # if not recent_knowledge: + # knowledge_info_str += "- 暂无。\n" # 更简洁的提示 - else: - knowledge_info_str += "- 暂无。\n" - except AttributeError: - logger.warning(f"[私聊][{self.private_name}]ConversationInfo 对象可能缺少 knowledge_list 属性。") - knowledge_info_str += "- 获取知识列表时出错。\n" - except Exception as e: - logger.error(f"[私聊][{self.private_name}]构建知识信息字符串时出错: {e}") - knowledge_info_str += "- 处理知识列表时出错。\n" + # else: + # knowledge_info_str += "- 暂无。\n" + # except AttributeError: + # logger.warning(f"[私聊][{self.private_name}]ConversationInfo 对象可能缺少 knowledge_list 属性。") + # knowledge_info_str += "- 获取知识列表时出错。\n" + # except Exception as e: + # logger.error(f"[私聊][{self.private_name}]构建知识信息字符串时出错: {e}") + # knowledge_info_str += "- 处理知识列表时出错。\n" # 获取聊天历史记录 (chat_history_text) chat_history_text = observation_info.chat_history_str @@ -377,7 +377,7 @@ class ReplyGenerator: persona_text=persona_text, goals_str=goals_str, chat_history_text=chat_history_text, - knowledge_info_str=knowledge_info_str, + # knowledge_info_str=knowledge_info_str, retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", # 如果为空则提示无 retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。" # 如果为空则提示无 ) From edda83453886ab2389af3d66e1aee31c2a0b319f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Fri, 2 May 2025 12:01:05 +0800 Subject: [PATCH 43/67] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4Traceback?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E4=B8=AD=E7=9A=84=20show=5Flocals=20?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E4=BB=A5=E7=AE=80=E5=8C=96=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E8=BF=BD=E8=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 2 +- src/common/database.py | 2 +- src/common/log_decorators.py | 2 +- src/common/server.py | 2 +- src/config/config.py | 2 +- src/do_tool/tool_can_use/base_tool.py | 2 +- src/individuality/individuality.py | 2 +- src/individuality/offline_llm.py | 2 +- src/main.py | 2 +- src/plugins/PFC/chat_observer.py | 2 +- src/plugins/PFC/conversation.py | 2 +- src/plugins/PFC/message_sender.py | 2 +- src/plugins/PFC/pfc.py | 2 +- src/plugins/chat/chat_stream.py | 2 +- src/plugins/chat/message.py | 2 +- src/plugins/chat/message_sender.py | 2 +- src/plugins/chat/utils_image.py | 2 +- src/plugins/config_reload/api.py | 2 +- src/plugins/emoji_system/emoji_manager.py | 2 +- src/plugins/heartFC_chat/heartFC_chat.py | 2 +- src/plugins/heartFC_chat/heartFC_sender.py | 2 +- src/plugins/knowledge/src/embedding_store.py | 2 +- src/plugins/memory_system/Hippocampus.py | 2 +- src/plugins/memory_system/debug_memory.py | 2 +- src/plugins/memory_system/manually_alter_memory.py | 2 +- src/plugins/memory_system/offline_llm.py | 2 +- src/plugins/memory_system/sample_distribution.py | 2 +- src/plugins/models/utils_model.py | 2 +- src/plugins/utils/prompt_builder.py | 2 +- src/plugins/utils/timer_calculator.py | 2 +- src/plugins/willing/willing_manager.py | 2 +- src/plugins/zhishi/knowledge_library.py | 2 +- 32 files changed, 32 insertions(+), 32 deletions(-) diff --git a/bot.py b/bot.py index 5d811d4e..41847a01 100644 --- a/bot.py +++ b/bot.py @@ -15,7 +15,7 @@ from src.common.crash_logger import install_crash_handler from src.main import MainSystem from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) logger = get_logger("main") diff --git a/src/common/database.py b/src/common/database.py index 66a2dc16..752f746d 100644 --- a/src/common/database.py +++ b/src/common/database.py @@ -3,7 +3,7 @@ from pymongo import MongoClient from pymongo.database import Database from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) _client = None _db = None diff --git a/src/common/log_decorators.py b/src/common/log_decorators.py index a57fae79..414ba923 100644 --- a/src/common/log_decorators.py +++ b/src/common/log_decorators.py @@ -4,7 +4,7 @@ from typing import Callable, Any from .logger import logger, add_custom_style_handler from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) def use_log_style( diff --git a/src/common/server.py b/src/common/server.py index c080e28a..ff6106a7 100644 --- a/src/common/server.py +++ b/src/common/server.py @@ -4,7 +4,7 @@ from uvicorn import Config, Server as UvicornServer import os from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) class Server: diff --git a/src/config/config.py b/src/config/config.py index a067633b..28d947ef 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -16,7 +16,7 @@ from packaging.specifiers import SpecifierSet, InvalidSpecifier from src.common.logger_manager import get_logger from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) # 配置主程序日志格式 diff --git a/src/do_tool/tool_can_use/base_tool.py b/src/do_tool/tool_can_use/base_tool.py index 88da036d..b0f04ffe 100644 --- a/src/do_tool/tool_can_use/base_tool.py +++ b/src/do_tool/tool_can_use/base_tool.py @@ -6,7 +6,7 @@ import os from src.common.logger_manager import get_logger from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) logger = get_logger("base_tool") diff --git a/src/individuality/individuality.py b/src/individuality/individuality.py index 963fae0e..38131ea1 100644 --- a/src/individuality/individuality.py +++ b/src/individuality/individuality.py @@ -4,7 +4,7 @@ from .identity import Identity import random from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) class Individuality: diff --git a/src/individuality/offline_llm.py b/src/individuality/offline_llm.py index 0e1a446c..cc956001 100644 --- a/src/individuality/offline_llm.py +++ b/src/individuality/offline_llm.py @@ -8,7 +8,7 @@ import requests from src.common.logger import get_module_logger from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) logger = get_module_logger("offline_llm") diff --git a/src/main.py b/src/main.py index 3de3e880..26a56ca2 100644 --- a/src/main.py +++ b/src/main.py @@ -19,7 +19,7 @@ from .individuality.individuality import Individuality from .common.server import global_server from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) logger = get_logger("main") diff --git a/src/plugins/PFC/chat_observer.py b/src/plugins/PFC/chat_observer.py index 34b66316..22cbf27d 100644 --- a/src/plugins/PFC/chat_observer.py +++ b/src/plugins/PFC/chat_observer.py @@ -9,7 +9,7 @@ from .chat_states import NotificationManager, create_new_message_notification, c from .message_storage import MongoDBMessageStorage from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) logger = get_module_logger("chat_observer") diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 925fd7b5..0bc4cae8 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -25,7 +25,7 @@ from .waiter import Waiter import traceback from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) logger = get_logger("pfc") diff --git a/src/plugins/PFC/message_sender.py b/src/plugins/PFC/message_sender.py index f1085768..12c2143e 100644 --- a/src/plugins/PFC/message_sender.py +++ b/src/plugins/PFC/message_sender.py @@ -10,7 +10,7 @@ from ..storage.storage import MessageStorage from ...config.config import global_config from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) logger = get_module_logger("message_sender") diff --git a/src/plugins/PFC/pfc.py b/src/plugins/PFC/pfc.py index 50f7bf4c..b17ee21d 100644 --- a/src/plugins/PFC/pfc.py +++ b/src/plugins/PFC/pfc.py @@ -10,7 +10,7 @@ from .observation_info import ObservationInfo from src.plugins.utils.chat_message_builder import build_readable_messages from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) if TYPE_CHECKING: pass diff --git a/src/plugins/chat/chat_stream.py b/src/plugins/chat/chat_stream.py index a949247c..53ebd502 100644 --- a/src/plugins/chat/chat_stream.py +++ b/src/plugins/chat/chat_stream.py @@ -11,7 +11,7 @@ from maim_message import GroupInfo, UserInfo from src.common.logger_manager import get_logger from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) logger = get_logger("chat_stream") diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py index 354082e1..b9c15288 100644 --- a/src/plugins/chat/message.py +++ b/src/plugins/chat/message.py @@ -11,7 +11,7 @@ from .utils_image import image_manager from maim_message import Seg, UserInfo, BaseMessageInfo, MessageBase from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) logger = get_logger("chat_message") diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index 8bfee44b..b65ae895 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -15,7 +15,7 @@ from .utils import truncate_message, calculate_typing_time, count_messages_betwe from src.common.logger_manager import get_logger from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) logger = get_logger("sender") diff --git a/src/plugins/chat/utils_image.py b/src/plugins/chat/utils_image.py index 1f734502..5508ad23 100644 --- a/src/plugins/chat/utils_image.py +++ b/src/plugins/chat/utils_image.py @@ -15,7 +15,7 @@ from ..models.utils_model import LLMRequest from src.common.logger_manager import get_logger from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) logger = get_logger("chat_image") diff --git a/src/plugins/config_reload/api.py b/src/plugins/config_reload/api.py index ee0a5454..56240b88 100644 --- a/src/plugins/config_reload/api.py +++ b/src/plugins/config_reload/api.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, HTTPException from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) # 创建APIRouter而不是FastAPI实例 router = APIRouter() diff --git a/src/plugins/emoji_system/emoji_manager.py b/src/plugins/emoji_system/emoji_manager.py index 24266c08..86dab9d9 100644 --- a/src/plugins/emoji_system/emoji_manager.py +++ b/src/plugins/emoji_system/emoji_manager.py @@ -17,7 +17,7 @@ from ..models.utils_model import LLMRequest from src.common.logger_manager import get_logger from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) logger = get_logger("emoji") diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 712b6af5..28c17d9a 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -29,7 +29,7 @@ from src.plugins.moods.moods import MoodManager from src.heart_flow.utils_chat import get_chat_type_and_target_info from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) WAITING_TIME_THRESHOLD = 300 # 等待新消息时间阈值,单位秒 diff --git a/src/plugins/heartFC_chat/heartFC_sender.py b/src/plugins/heartFC_chat/heartFC_sender.py index 6fab5d62..b193ae44 100644 --- a/src/plugins/heartFC_chat/heartFC_sender.py +++ b/src/plugins/heartFC_chat/heartFC_sender.py @@ -11,7 +11,7 @@ from src.common.logger_manager import get_logger from src.plugins.chat.utils import calculate_typing_time from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) logger = get_logger("sender") diff --git a/src/plugins/knowledge/src/embedding_store.py b/src/plugins/knowledge/src/embedding_store.py index 72c6c7b5..8e0d116b 100644 --- a/src/plugins/knowledge/src/embedding_store.py +++ b/src/plugins/knowledge/src/embedding_store.py @@ -14,7 +14,7 @@ from .utils.hash import get_sha256 from .global_logger import logger from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) @dataclass diff --git a/src/plugins/memory_system/Hippocampus.py b/src/plugins/memory_system/Hippocampus.py index 11ba8f40..24d320f7 100644 --- a/src/plugins/memory_system/Hippocampus.py +++ b/src/plugins/memory_system/Hippocampus.py @@ -22,7 +22,7 @@ from ..chat.utils import translate_timestamp_to_human_readable from .memory_config import MemoryConfig from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) def calculate_information_content(text): diff --git a/src/plugins/memory_system/debug_memory.py b/src/plugins/memory_system/debug_memory.py index ae767c85..8f79c6a8 100644 --- a/src/plugins/memory_system/debug_memory.py +++ b/src/plugins/memory_system/debug_memory.py @@ -10,7 +10,7 @@ from src.plugins.memory_system.Hippocampus import HippocampusManager from src.config.config import global_config from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) async def test_memory_system(): diff --git a/src/plugins/memory_system/manually_alter_memory.py b/src/plugins/memory_system/manually_alter_memory.py index 10a75738..ce5abbba 100644 --- a/src/plugins/memory_system/manually_alter_memory.py +++ b/src/plugins/memory_system/manually_alter_memory.py @@ -11,7 +11,7 @@ from Hippocampus import Hippocampus # 海马体和记忆图 from dotenv import load_dotenv from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) """ diff --git a/src/plugins/memory_system/offline_llm.py b/src/plugins/memory_system/offline_llm.py index 335a76d3..d4862ad3 100644 --- a/src/plugins/memory_system/offline_llm.py +++ b/src/plugins/memory_system/offline_llm.py @@ -8,7 +8,7 @@ import requests from src.common.logger import get_module_logger from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) logger = get_module_logger("offline_llm") diff --git a/src/plugins/memory_system/sample_distribution.py b/src/plugins/memory_system/sample_distribution.py index 76796728..b3b84eb4 100644 --- a/src/plugins/memory_system/sample_distribution.py +++ b/src/plugins/memory_system/sample_distribution.py @@ -3,7 +3,7 @@ from scipy import stats from datetime import datetime, timedelta from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) class DistributionVisualizer: diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py index 7c7fe713..8ee21956 100644 --- a/src/plugins/models/utils_model.py +++ b/src/plugins/models/utils_model.py @@ -16,7 +16,7 @@ from ...common.database import db from ...config.config import global_config from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) logger = get_module_logger("model_utils") diff --git a/src/plugins/utils/prompt_builder.py b/src/plugins/utils/prompt_builder.py index c4555a55..4a226a02 100644 --- a/src/plugins/utils/prompt_builder.py +++ b/src/plugins/utils/prompt_builder.py @@ -7,7 +7,7 @@ from src.common.logger import get_module_logger # import traceback from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) logger = get_module_logger("prompt_build") diff --git a/src/plugins/utils/timer_calculator.py b/src/plugins/utils/timer_calculator.py index d66f21cc..af8058a5 100644 --- a/src/plugins/utils/timer_calculator.py +++ b/src/plugins/utils/timer_calculator.py @@ -4,7 +4,7 @@ from typing import Optional, Dict, Callable import asyncio from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) """ # 更好的计时器 diff --git a/src/plugins/willing/willing_manager.py b/src/plugins/willing/willing_manager.py index a5884da2..ba1e3e09 100644 --- a/src/plugins/willing/willing_manager.py +++ b/src/plugins/willing/willing_manager.py @@ -10,7 +10,7 @@ from typing import Dict, Optional import asyncio from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) """ 基类方法概览: diff --git a/src/plugins/zhishi/knowledge_library.py b/src/plugins/zhishi/knowledge_library.py index 26af3bda..6fa1d3e1 100644 --- a/src/plugins/zhishi/knowledge_library.py +++ b/src/plugins/zhishi/knowledge_library.py @@ -9,7 +9,7 @@ from rich.console import Console from rich.table import Table from rich.traceback import install -install(show_locals=True, extra_lines=3) +install(extra_lines=3) # 添加项目根目录到 Python 路径 root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) From 03961b71a259f52fb79e6b47565c6fed1db00fe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Fri, 2 May 2025 13:42:28 +0800 Subject: [PATCH 44/67] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E8=B7=AF=E5=BE=84=E9=85=8D=E7=BD=AE=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E6=95=B0=E6=8D=AE=E5=A4=84=E7=90=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96=E9=94=99=E8=AF=AF=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/import_openie.py | 6 +- scripts/info_extraction.py | 103 +++++++++++++------ scripts/raw_data_preprocessor.py | 30 ++++-- src/plugins/knowledge/knowledge_lib.py | 2 + src/plugins/knowledge/src/embedding_store.py | 45 +++++--- src/plugins/knowledge/src/kg_manager.py | 79 ++++++++------ src/plugins/knowledge/src/open_ie.py | 48 +++++++-- src/plugins/knowledge/src/raw_processing.py | 18 ++-- template/lpmm_config_template.toml | 4 +- 9 files changed, 226 insertions(+), 109 deletions(-) diff --git a/scripts/import_openie.py b/scripts/import_openie.py index 595f22ec..25a1a877 100644 --- a/scripts/import_openie.py +++ b/scripts/import_openie.py @@ -19,7 +19,8 @@ from src.plugins.knowledge.src.utils.hash import get_sha256 # 添加项目根目录到 sys.path - +ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +OPENIE_DIR = global_config["persistence"]["openie_data_path"] if global_config["persistence"]["openie_data_path"] else os.path.join(ROOT_PATH, "data/openie") logger = get_module_logger("LPMM知识库-OpenIE导入") @@ -131,6 +132,7 @@ def main(): embed_manager.load_from_file() except Exception as e: logger.error("从文件加载Embedding库时发生错误:{}".format(e)) + logger.error("如果你是第一次导入知识,请忽略此错误") logger.info("Embedding库加载完成") # 初始化KG kg_manager = KGManager() @@ -139,6 +141,7 @@ def main(): kg_manager.load_from_file() except Exception as e: logger.error("从文件加载KG时发生错误:{}".format(e)) + logger.error("如果你是第一次导入知识,请忽略此错误") logger.info("KG加载完成") logger.info(f"KG节点数量:{len(kg_manager.graph.get_node_list())}") @@ -163,4 +166,5 @@ def main(): if __name__ == "__main__": + # logger.info(f"111111111111111111111111{ROOT_PATH}") main() diff --git a/scripts/info_extraction.py b/scripts/info_extraction.py index 65c4082b..00f7a2a2 100644 --- a/scripts/info_extraction.py +++ b/scripts/info_extraction.py @@ -4,11 +4,13 @@ import signal from concurrent.futures import ThreadPoolExecutor, as_completed from threading import Lock, Event import sys +import glob +import datetime sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) # 添加项目根目录到 sys.path -import tqdm +from rich.progress import Progress # 替换为 rich 进度条 from src.common.logger import get_module_logger from src.plugins.knowledge.src.lpmmconfig import global_config @@ -16,10 +18,15 @@ from src.plugins.knowledge.src.ie_process import info_extract_from_str from src.plugins.knowledge.src.llm_client import LLMClient from src.plugins.knowledge.src.open_ie import OpenIE from src.plugins.knowledge.src.raw_processing import load_raw_data +from rich.progress import BarColumn, TimeElapsedColumn, TimeRemainingColumn, TaskProgressColumn, MofNCompleteColumn, SpinnerColumn, TextColumn logger = get_module_logger("LPMM知识库-信息提取") -TEMP_DIR = "./temp" + +ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +TEMP_DIR = os.path.join(ROOT_PATH, "temp") +IMPORTED_DATA_PATH = global_config["persistence"]["raw_data_path"] if global_config["persistence"]["raw_data_path"] else os.path.join(ROOT_PATH, "data/imported_lpmm_data") +OPENIE_OUTPUT_DIR = global_config["persistence"]["openie_data_path"] if global_config["persistence"]["openie_data_path"] else os.path.join(ROOT_PATH, "data/openie") # 创建一个线程安全的锁,用于保护文件操作和共享数据 file_lock = Lock() @@ -70,8 +77,7 @@ def process_single_text(pg_hash, raw_data, llm_client_list): # 如果保存失败,确保不会留下损坏的文件 if os.path.exists(temp_file_path): os.remove(temp_file_path) - # 设置shutdown_event以终止程序 - shutdown_event.set() + sys.exit(0) return None, pg_hash return doc_item, None @@ -79,7 +85,7 @@ def process_single_text(pg_hash, raw_data, llm_client_list): def signal_handler(_signum, _frame): """处理Ctrl+C信号""" logger.info("\n接收到中断信号,正在优雅地关闭程序...") - shutdown_event.set() + sys.exit(0) def main(): @@ -110,33 +116,61 @@ def main(): global_config["llm_providers"][key]["api_key"], ) - logger.info("正在加载原始数据") - sha256_list, raw_datas = load_raw_data() - logger.info("原始数据加载完成\n") + # 检查 openie 输出目录 + if not os.path.exists(OPENIE_OUTPUT_DIR): + os.makedirs(OPENIE_OUTPUT_DIR) + logger.info(f"已创建输出目录: {OPENIE_OUTPUT_DIR}") - # 创建临时目录 - if not os.path.exists(f"{TEMP_DIR}"): - os.makedirs(f"{TEMP_DIR}") + # 确保 TEMP_DIR 目录存在 + if not os.path.exists(TEMP_DIR): + os.makedirs(TEMP_DIR) + logger.info(f"已创建缓存目录: {TEMP_DIR}") + + # 遍历IMPORTED_DATA_PATH下所有json文件 + imported_files = sorted(glob.glob(os.path.join(IMPORTED_DATA_PATH, "*.json"))) + if not imported_files: + logger.error(f"未在 {IMPORTED_DATA_PATH} 下找到任何json文件") + sys.exit(1) + + all_sha256_list = [] + all_raw_datas = [] + + for imported_file in imported_files: + logger.info(f"正在处理文件: {imported_file}") + try: + sha256_list, raw_datas = load_raw_data(imported_file) + except Exception as e: + logger.error(f"读取文件失败: {imported_file}, 错误: {e}") + continue + all_sha256_list.extend(sha256_list) + all_raw_datas.extend(raw_datas) failed_sha256 = [] open_ie_doc = [] - # 创建线程池,最大线程数为50 workers = global_config["info_extraction"]["workers"] with ThreadPoolExecutor(max_workers=workers) as executor: - # 提交所有任务到线程池 future_to_hash = { executor.submit(process_single_text, pg_hash, raw_data, llm_client_list): pg_hash - for pg_hash, raw_data in zip(sha256_list, raw_datas) + for pg_hash, raw_data in zip(all_sha256_list, all_raw_datas) } - # 使用tqdm显示进度 - with tqdm.tqdm(total=len(future_to_hash), postfix="正在进行提取:") as pbar: - # 处理完成的任务 + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + BarColumn(), + TaskProgressColumn(), + MofNCompleteColumn(), + "•", + TimeElapsedColumn(), + "<", + TimeRemainingColumn(), + transient=False, + ) as progress: + task = progress.add_task("正在进行提取:", total=len(future_to_hash)) try: for future in as_completed(future_to_hash): if shutdown_event.is_set(): - # 取消所有未完成的任务 for f in future_to_hash: if not f.done(): f.cancel() @@ -149,26 +183,33 @@ def main(): elif doc_item: with open_ie_doc_lock: open_ie_doc.append(doc_item) - pbar.update(1) + progress.update(task, advance=1) except KeyboardInterrupt: - # 如果在这里捕获到KeyboardInterrupt,说明signal_handler可能没有正常工作 logger.info("\n接收到中断信号,正在优雅地关闭程序...") shutdown_event.set() - # 取消所有未完成的任务 for f in future_to_hash: if not f.done(): f.cancel() - # 保存信息提取结果 - sum_phrase_chars = sum([len(e) for chunk in open_ie_doc for e in chunk["extracted_entities"]]) - sum_phrase_words = sum([len(e.split()) for chunk in open_ie_doc for e in chunk["extracted_entities"]]) - num_phrases = sum([len(chunk["extracted_entities"]) for chunk in open_ie_doc]) - openie_obj = OpenIE( - open_ie_doc, - round(sum_phrase_chars / num_phrases, 4), - round(sum_phrase_words / num_phrases, 4), - ) - OpenIE.save(openie_obj) + # 合并所有文件的提取结果并保存 + if open_ie_doc: + sum_phrase_chars = sum([len(e) for chunk in open_ie_doc for e in chunk["extracted_entities"]]) + sum_phrase_words = sum([len(e.split()) for chunk in open_ie_doc for e in chunk["extracted_entities"]]) + num_phrases = sum([len(chunk["extracted_entities"]) for chunk in open_ie_doc]) + openie_obj = OpenIE( + open_ie_doc, + round(sum_phrase_chars / num_phrases, 4) if num_phrases else 0, + round(sum_phrase_words / num_phrases, 4) if num_phrases else 0, + ) + # 输出文件名格式:MM-DD-HH-ss-openie.json + now = datetime.datetime.now() + filename = now.strftime("%m-%d-%H-%S-openie.json") + output_path = os.path.join(OPENIE_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + json.dump(openie_obj.to_dict() if hasattr(openie_obj, "to_dict") else openie_obj.__dict__, f, ensure_ascii=False, indent=4) + logger.info(f"信息提取结果已保存到: {output_path}") + else: + logger.warning("没有可保存的信息提取结果") logger.info("--------信息提取完成--------") logger.info(f"提取失败的文段SHA256:{failed_sha256}") diff --git a/scripts/raw_data_preprocessor.py b/scripts/raw_data_preprocessor.py index 2fc30352..d808fb0e 100644 --- a/scripts/raw_data_preprocessor.py +++ b/scripts/raw_data_preprocessor.py @@ -2,18 +2,22 @@ import json import os from pathlib import Path import sys # 新增系统模块导入 +import datetime # 新增导入 sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) from src.common.logger import get_module_logger logger = get_module_logger("LPMM数据库-原始数据处理") +ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +RAW_DATA_PATH = os.path.join(ROOT_PATH, "data/lpmm_raw_data") +IMPORTED_DATA_PATH = os.path.join(ROOT_PATH, "data/imported_lpmm_data") # 添加项目根目录到 sys.path def check_and_create_dirs(): """检查并创建必要的目录""" - required_dirs = ["data/lpmm_raw_data", "data/imported_lpmm_data"] + required_dirs = [RAW_DATA_PATH, IMPORTED_DATA_PATH] for dir_path in required_dirs: if not os.path.exists(dir_path): @@ -58,17 +62,17 @@ def main(): # 检查并创建必要的目录 check_and_create_dirs() - # 检查输出文件是否存在 - if os.path.exists("data/import.json"): - logger.error("错误: data/import.json 已存在,请先处理或删除该文件") - sys.exit(1) + # # 检查输出文件是否存在 + # if os.path.exists(RAW_DATA_PATH): + # logger.error("错误: data/import.json 已存在,请先处理或删除该文件") + # sys.exit(1) - if os.path.exists("data/openie.json"): - logger.error("错误: data/openie.json 已存在,请先处理或删除该文件") - sys.exit(1) + # if os.path.exists(RAW_DATA_PATH): + # logger.error("错误: data/openie.json 已存在,请先处理或删除该文件") + # sys.exit(1) # 获取所有原始文本文件 - raw_files = list(Path("data/lpmm_raw_data").glob("*.txt")) + raw_files = list(Path(RAW_DATA_PATH).glob("*.txt")) if not raw_files: logger.warning("警告: data/lpmm_raw_data 中没有找到任何 .txt 文件") sys.exit(1) @@ -80,8 +84,10 @@ def main(): paragraphs = process_text_file(file) all_paragraphs.extend(paragraphs) - # 保存合并后的结果 - output_path = "data/import.json" + # 保存合并后的结果到 IMPORTED_DATA_PATH,文件名格式为 MM-DD-HH-ss-imported-data.json + now = datetime.datetime.now() + filename = now.strftime("%m-%d-%H-%S-imported-data.json") + output_path = os.path.join(IMPORTED_DATA_PATH, filename) with open(output_path, "w", encoding="utf-8") as f: json.dump(all_paragraphs, f, ensure_ascii=False, indent=4) @@ -89,4 +95,6 @@ def main(): if __name__ == "__main__": + print(f"Raw Data Path: {RAW_DATA_PATH}") + print(f"Imported Data Path: {IMPORTED_DATA_PATH}") main() diff --git a/src/plugins/knowledge/knowledge_lib.py b/src/plugins/knowledge/knowledge_lib.py index c0d2fe61..df82970a 100644 --- a/src/plugins/knowledge/knowledge_lib.py +++ b/src/plugins/knowledge/knowledge_lib.py @@ -26,6 +26,7 @@ try: embed_manager.load_from_file() except Exception as e: logger.error("从文件加载Embedding库时发生错误:{}".format(e)) + logger.error("如果你是第一次导入知识,或者还未导入知识,请忽略此错误") logger.info("Embedding库加载完成") # 初始化KG kg_manager = KGManager() @@ -34,6 +35,7 @@ try: kg_manager.load_from_file() except Exception as e: logger.error("从文件加载KG时发生错误:{}".format(e)) + logger.error("如果你是第一次导入知识,或者还未导入知识,请忽略此错误") logger.info("KG加载完成") logger.info(f"KG节点数量:{len(kg_manager.graph.get_node_list())}") diff --git a/src/plugins/knowledge/src/embedding_store.py b/src/plugins/knowledge/src/embedding_store.py index 8e0d116b..d2791ca4 100644 --- a/src/plugins/knowledge/src/embedding_store.py +++ b/src/plugins/knowledge/src/embedding_store.py @@ -13,9 +13,11 @@ from .lpmmconfig import ENT_NAMESPACE, PG_NAMESPACE, REL_NAMESPACE, global_confi from .utils.hash import get_sha256 from .global_logger import logger from rich.traceback import install +from rich.progress import Progress, BarColumn, TimeElapsedColumn, TimeRemainingColumn, TaskProgressColumn, MofNCompleteColumn, SpinnerColumn, TextColumn install(extra_lines=3) +TOTAL_EMBEDDING_TIMES = 3 # 统计嵌入次数 @dataclass class EmbeddingStoreItem: @@ -52,20 +54,35 @@ class EmbeddingStore: def _get_embedding(self, s: str) -> List[float]: return self.llm_client.send_embedding_request(global_config["embedding"]["model"], s) - def batch_insert_strs(self, strs: List[str]) -> None: + def batch_insert_strs(self, strs: List[str], times: int) -> None: """向库中存入字符串""" - # 逐项处理 - for s in tqdm.tqdm(strs, desc="存入嵌入库", unit="items"): - # 计算hash去重 - item_hash = self.namespace + "-" + get_sha256(s) - if item_hash in self.store: - continue + total = len(strs) + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + BarColumn(), + TaskProgressColumn(), + MofNCompleteColumn(), + "•", + TimeElapsedColumn(), + "<", + TimeRemainingColumn(), + transient=False, + ) as progress: + task = progress.add_task(f"存入嵌入库:({times}/{TOTAL_EMBEDDING_TIMES})", total=total) + for s in strs: + # 计算hash去重 + item_hash = self.namespace + "-" + get_sha256(s) + if item_hash in self.store: + progress.update(task, advance=1) + continue - # 获取embedding - embedding = self._get_embedding(s) + # 获取embedding + embedding = self._get_embedding(s) - # 存入 - self.store[item_hash] = EmbeddingStoreItem(item_hash, embedding, s) + # 存入 + self.store[item_hash] = EmbeddingStoreItem(item_hash, embedding, s) + progress.update(task, advance=1) def save_to_file(self) -> None: """保存到文件""" @@ -191,7 +208,7 @@ class EmbeddingManager: def _store_pg_into_embedding(self, raw_paragraphs: Dict[str, str]): """将段落编码存入Embedding库""" - self.paragraphs_embedding_store.batch_insert_strs(list(raw_paragraphs.values())) + self.paragraphs_embedding_store.batch_insert_strs(list(raw_paragraphs.values()),times=1) def _store_ent_into_embedding(self, triple_list_data: Dict[str, List[List[str]]]): """将实体编码存入Embedding库""" @@ -200,7 +217,7 @@ class EmbeddingManager: for triple in triple_list: entities.add(triple[0]) entities.add(triple[2]) - self.entities_embedding_store.batch_insert_strs(list(entities)) + self.entities_embedding_store.batch_insert_strs(list(entities),times=2) def _store_rel_into_embedding(self, triple_list_data: Dict[str, List[List[str]]]): """将关系编码存入Embedding库""" @@ -208,7 +225,7 @@ class EmbeddingManager: for triples in triple_list_data.values(): graph_triples.extend([tuple(t) for t in triples]) graph_triples = list(set(graph_triples)) - self.relation_embedding_store.batch_insert_strs([str(triple) for triple in graph_triples]) + self.relation_embedding_store.batch_insert_strs([str(triple) for triple in graph_triples],times=3) def load_from_file(self): """从文件加载""" diff --git a/src/plugins/knowledge/src/kg_manager.py b/src/plugins/knowledge/src/kg_manager.py index 71ce65ef..ccaf7aa8 100644 --- a/src/plugins/knowledge/src/kg_manager.py +++ b/src/plugins/knowledge/src/kg_manager.py @@ -5,7 +5,7 @@ from typing import Dict, List, Tuple import numpy as np import pandas as pd -import tqdm +from rich.progress import Progress, BarColumn, TimeElapsedColumn, TimeRemainingColumn, TaskProgressColumn, MofNCompleteColumn, SpinnerColumn, TextColumn from quick_algo import di_graph, pagerank @@ -132,41 +132,56 @@ class KGManager: ent_hash_list = list(ent_hash_list) synonym_hash_set = set() - synonym_result = dict() - # 对每个实体节点,查找其相似的实体节点,建立扩展连接 - for ent_hash in tqdm.tqdm(ent_hash_list): - if ent_hash in synonym_hash_set: - # 避免同一批次内重复添加 - continue - ent = embedding_manager.entities_embedding_store.store.get(ent_hash) - assert isinstance(ent, EmbeddingStoreItem) - if ent is None: - continue - # 查询相似实体 - similar_ents = embedding_manager.entities_embedding_store.search_top_k( - ent.embedding, global_config["rag"]["params"]["synonym_search_top_k"] - ) - res_ent = [] # Debug - for res_ent_hash, similarity in similar_ents: - if res_ent_hash == ent_hash: - # 避免自连接 + # rich 进度条 + total = len(ent_hash_list) + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + BarColumn(), + TaskProgressColumn(), + MofNCompleteColumn(), + "•", + TimeElapsedColumn(), + "<", + TimeRemainingColumn(), + transient=False, + ) as progress: + task = progress.add_task("同义词连接", total=total) + for ent_hash in ent_hash_list: + if ent_hash in synonym_hash_set: + progress.update(task, advance=1) continue - if similarity < global_config["rag"]["params"]["synonym_threshold"]: - # 相似度阈值 + ent = embedding_manager.entities_embedding_store.store.get(ent_hash) + assert isinstance(ent, EmbeddingStoreItem) + if ent is None: + progress.update(task, advance=1) continue - node_to_node[(res_ent_hash, ent_hash)] = similarity - node_to_node[(ent_hash, res_ent_hash)] = similarity - synonym_hash_set.add(res_ent_hash) - new_edge_cnt += 1 - res_ent.append( - ( - embedding_manager.entities_embedding_store.store[res_ent_hash].str, - similarity, - ) - ) # Debug - synonym_result[ent.str] = res_ent + # 查询相似实体 + similar_ents = embedding_manager.entities_embedding_store.search_top_k( + ent.embedding, global_config["rag"]["params"]["synonym_search_top_k"] + ) + res_ent = [] # Debug + for res_ent_hash, similarity in similar_ents: + if res_ent_hash == ent_hash: + # 避免自连接 + continue + if similarity < global_config["rag"]["params"]["synonym_threshold"]: + # 相似度阈值 + continue + node_to_node[(res_ent_hash, ent_hash)] = similarity + node_to_node[(ent_hash, res_ent_hash)] = similarity + synonym_hash_set.add(res_ent_hash) + new_edge_cnt += 1 + res_ent.append( + ( + embedding_manager.entities_embedding_store.store[res_ent_hash].str, + similarity, + ) + ) # Debug + synonym_result[ent.str] = res_ent + progress.update(task, advance=1) for k, v in synonym_result.items(): print(f'"{k}"的相似实体为:{v}') diff --git a/src/plugins/knowledge/src/open_ie.py b/src/plugins/knowledge/src/open_ie.py index 5fe163bb..ea84af4a 100644 --- a/src/plugins/knowledge/src/open_ie.py +++ b/src/plugins/knowledge/src/open_ie.py @@ -1,9 +1,13 @@ import json +import os +import glob from typing import Any, Dict, List from .lpmmconfig import INVALID_ENTITY, global_config +ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + def _filter_invalid_entities(entities: List[str]) -> List[str]: """过滤无效的实体""" @@ -74,12 +78,22 @@ class OpenIE: doc["extracted_triples"] = _filter_invalid_triples(doc["extracted_triples"]) @staticmethod - def _from_dict(data): - """从字典中获取OpenIE对象""" + def _from_dict(data_list): + """从多个字典合并OpenIE对象""" + # data_list: List[dict] + all_docs = [] + for data in data_list: + all_docs.extend(data.get("docs", [])) + # 重新计算统计 + sum_phrase_chars = sum([len(e) for chunk in all_docs for e in chunk["extracted_entities"]]) + sum_phrase_words = sum([len(e.split()) for chunk in all_docs for e in chunk["extracted_entities"]]) + num_phrases = sum([len(chunk["extracted_entities"]) for chunk in all_docs]) + avg_ent_chars = round(sum_phrase_chars / num_phrases, 4) if num_phrases else 0 + avg_ent_words = round(sum_phrase_words / num_phrases, 4) if num_phrases else 0 return OpenIE( - docs=data["docs"], - avg_ent_chars=data["avg_ent_chars"], - avg_ent_words=data["avg_ent_words"], + docs=all_docs, + avg_ent_chars=avg_ent_chars, + avg_ent_words=avg_ent_words, ) def _to_dict(self): @@ -92,12 +106,20 @@ class OpenIE: @staticmethod def load() -> "OpenIE": - """从文件中加载OpenIE数据""" - with open(global_config["persistence"]["openie_data_path"], "r", encoding="utf-8") as f: - data = json.loads(f.read()) - - openie_data = OpenIE._from_dict(data) - + """从OPENIE_DIR下所有json文件合并加载OpenIE数据""" + openie_dir = os.path.join(ROOT_PATH, global_config["persistence"]["openie_data_path"]) + if not os.path.exists(openie_dir): + raise Exception(f"OpenIE数据目录不存在: {openie_dir}") + json_files = sorted(glob.glob(os.path.join(openie_dir, "*.json"))) + data_list = [] + for file in json_files: + with open(file, "r", encoding="utf-8") as f: + data = json.load(f) + data_list.append(data) + if not data_list: + # print(f"111111111111111111111Root Path : \n{ROOT_PATH}") + raise Exception(f"未在 {openie_dir} 找到任何OpenIE json文件") + openie_data = OpenIE._from_dict(data_list) return openie_data @staticmethod @@ -132,3 +154,7 @@ class OpenIE: """提取原始段落""" raw_paragraph_dict = dict({doc_item["idx"]: doc_item["passage"] for doc_item in self.docs}) return raw_paragraph_dict + +if __name__ == "__main__": + # 测试代码 + print(ROOT_PATH) diff --git a/src/plugins/knowledge/src/raw_processing.py b/src/plugins/knowledge/src/raw_processing.py index 91e681c7..a333ef99 100644 --- a/src/plugins/knowledge/src/raw_processing.py +++ b/src/plugins/knowledge/src/raw_processing.py @@ -6,21 +6,25 @@ from .lpmmconfig import global_config from .utils.hash import get_sha256 -def load_raw_data() -> tuple[list[str], list[str]]: +def load_raw_data(path: str = None) -> tuple[list[str], list[str]]: """加载原始数据文件 读取原始数据文件,将原始数据加载到内存中 + Args: + path: 可选,指定要读取的json文件绝对路径 + Returns: - - raw_data: 原始数据字典 - - md5_set: 原始数据的SHA256集合 + - raw_data: 原始数据列表 + - sha256_list: 原始数据的SHA256集合 """ - # 读取import.json文件 - if os.path.exists(global_config["persistence"]["raw_data_path"]) is True: - with open(global_config["persistence"]["raw_data_path"], "r", encoding="utf-8") as f: + # 读取指定路径或默认路径的json文件 + json_path = path if path else global_config["persistence"]["raw_data_path"] + if os.path.exists(json_path): + with open(json_path, "r", encoding="utf-8") as f: import_json = json.loads(f.read()) else: - raise Exception("原始数据文件读取失败") + raise Exception(f"原始数据文件读取失败: {json_path}") # import_json内容示例: # import_json = [ # "The capital of China is Beijing. The capital of France is Paris.", diff --git a/template/lpmm_config_template.toml b/template/lpmm_config_template.toml index 43785e79..8563b7ca 100644 --- a/template/lpmm_config_template.toml +++ b/template/lpmm_config_template.toml @@ -51,7 +51,7 @@ res_top_k = 3 # 最终提供的文段TopK [persistence] # 持久化配置(存储中间数据,防止重复计算) data_root_path = "data" # 数据根目录 -raw_data_path = "data/import.json" # 原始数据路径 -openie_data_path = "data/openie.json" # OpenIE数据路径 +raw_data_path = "data/imported_lpmm_data" # 原始数据路径 +openie_data_path = "data/openie" # OpenIE数据路径 embedding_data_dir = "data/embedding" # 嵌入数据目录 rag_data_dir = "data/rag" # RAG数据目录 From b117e876870e0743b06efa4000b6aba01f0a0588 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 2 May 2025 05:42:41 +0000 Subject: [PATCH 45/67] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/import_openie.py | 6 +++- scripts/info_extraction.py | 29 +++++++++++++++++--- src/plugins/knowledge/src/embedding_store.py | 18 +++++++++--- src/plugins/knowledge/src/kg_manager.py | 11 +++++++- src/plugins/knowledge/src/open_ie.py | 3 +- 5 files changed, 56 insertions(+), 11 deletions(-) diff --git a/scripts/import_openie.py b/scripts/import_openie.py index 25a1a877..dd4b50ec 100644 --- a/scripts/import_openie.py +++ b/scripts/import_openie.py @@ -20,7 +20,11 @@ from src.plugins.knowledge.src.utils.hash import get_sha256 # 添加项目根目录到 sys.path ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) -OPENIE_DIR = global_config["persistence"]["openie_data_path"] if global_config["persistence"]["openie_data_path"] else os.path.join(ROOT_PATH, "data/openie") +OPENIE_DIR = ( + global_config["persistence"]["openie_data_path"] + if global_config["persistence"]["openie_data_path"] + else os.path.join(ROOT_PATH, "data/openie") +) logger = get_module_logger("LPMM知识库-OpenIE导入") diff --git a/scripts/info_extraction.py b/scripts/info_extraction.py index 00f7a2a2..ee0d789a 100644 --- a/scripts/info_extraction.py +++ b/scripts/info_extraction.py @@ -18,15 +18,31 @@ from src.plugins.knowledge.src.ie_process import info_extract_from_str from src.plugins.knowledge.src.llm_client import LLMClient from src.plugins.knowledge.src.open_ie import OpenIE from src.plugins.knowledge.src.raw_processing import load_raw_data -from rich.progress import BarColumn, TimeElapsedColumn, TimeRemainingColumn, TaskProgressColumn, MofNCompleteColumn, SpinnerColumn, TextColumn +from rich.progress import ( + BarColumn, + TimeElapsedColumn, + TimeRemainingColumn, + TaskProgressColumn, + MofNCompleteColumn, + SpinnerColumn, + TextColumn, +) logger = get_module_logger("LPMM知识库-信息提取") ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) TEMP_DIR = os.path.join(ROOT_PATH, "temp") -IMPORTED_DATA_PATH = global_config["persistence"]["raw_data_path"] if global_config["persistence"]["raw_data_path"] else os.path.join(ROOT_PATH, "data/imported_lpmm_data") -OPENIE_OUTPUT_DIR = global_config["persistence"]["openie_data_path"] if global_config["persistence"]["openie_data_path"] else os.path.join(ROOT_PATH, "data/openie") +IMPORTED_DATA_PATH = ( + global_config["persistence"]["raw_data_path"] + if global_config["persistence"]["raw_data_path"] + else os.path.join(ROOT_PATH, "data/imported_lpmm_data") +) +OPENIE_OUTPUT_DIR = ( + global_config["persistence"]["openie_data_path"] + if global_config["persistence"]["openie_data_path"] + else os.path.join(ROOT_PATH, "data/openie") +) # 创建一个线程安全的锁,用于保护文件操作和共享数据 file_lock = Lock() @@ -206,7 +222,12 @@ def main(): filename = now.strftime("%m-%d-%H-%S-openie.json") output_path = os.path.join(OPENIE_OUTPUT_DIR, filename) with open(output_path, "w", encoding="utf-8") as f: - json.dump(openie_obj.to_dict() if hasattr(openie_obj, "to_dict") else openie_obj.__dict__, f, ensure_ascii=False, indent=4) + json.dump( + openie_obj.to_dict() if hasattr(openie_obj, "to_dict") else openie_obj.__dict__, + f, + ensure_ascii=False, + indent=4, + ) logger.info(f"信息提取结果已保存到: {output_path}") else: logger.warning("没有可保存的信息提取结果") diff --git a/src/plugins/knowledge/src/embedding_store.py b/src/plugins/knowledge/src/embedding_store.py index d2791ca4..e734f4e9 100644 --- a/src/plugins/knowledge/src/embedding_store.py +++ b/src/plugins/knowledge/src/embedding_store.py @@ -13,12 +13,22 @@ from .lpmmconfig import ENT_NAMESPACE, PG_NAMESPACE, REL_NAMESPACE, global_confi from .utils.hash import get_sha256 from .global_logger import logger from rich.traceback import install -from rich.progress import Progress, BarColumn, TimeElapsedColumn, TimeRemainingColumn, TaskProgressColumn, MofNCompleteColumn, SpinnerColumn, TextColumn +from rich.progress import ( + Progress, + BarColumn, + TimeElapsedColumn, + TimeRemainingColumn, + TaskProgressColumn, + MofNCompleteColumn, + SpinnerColumn, + TextColumn, +) install(extra_lines=3) TOTAL_EMBEDDING_TIMES = 3 # 统计嵌入次数 + @dataclass class EmbeddingStoreItem: """嵌入库中的项""" @@ -208,7 +218,7 @@ class EmbeddingManager: def _store_pg_into_embedding(self, raw_paragraphs: Dict[str, str]): """将段落编码存入Embedding库""" - self.paragraphs_embedding_store.batch_insert_strs(list(raw_paragraphs.values()),times=1) + self.paragraphs_embedding_store.batch_insert_strs(list(raw_paragraphs.values()), times=1) def _store_ent_into_embedding(self, triple_list_data: Dict[str, List[List[str]]]): """将实体编码存入Embedding库""" @@ -217,7 +227,7 @@ class EmbeddingManager: for triple in triple_list: entities.add(triple[0]) entities.add(triple[2]) - self.entities_embedding_store.batch_insert_strs(list(entities),times=2) + self.entities_embedding_store.batch_insert_strs(list(entities), times=2) def _store_rel_into_embedding(self, triple_list_data: Dict[str, List[List[str]]]): """将关系编码存入Embedding库""" @@ -225,7 +235,7 @@ class EmbeddingManager: for triples in triple_list_data.values(): graph_triples.extend([tuple(t) for t in triples]) graph_triples = list(set(graph_triples)) - self.relation_embedding_store.batch_insert_strs([str(triple) for triple in graph_triples],times=3) + self.relation_embedding_store.batch_insert_strs([str(triple) for triple in graph_triples], times=3) def load_from_file(self): """从文件加载""" diff --git a/src/plugins/knowledge/src/kg_manager.py b/src/plugins/knowledge/src/kg_manager.py index ccaf7aa8..fd922af4 100644 --- a/src/plugins/knowledge/src/kg_manager.py +++ b/src/plugins/knowledge/src/kg_manager.py @@ -5,7 +5,16 @@ from typing import Dict, List, Tuple import numpy as np import pandas as pd -from rich.progress import Progress, BarColumn, TimeElapsedColumn, TimeRemainingColumn, TaskProgressColumn, MofNCompleteColumn, SpinnerColumn, TextColumn +from rich.progress import ( + Progress, + BarColumn, + TimeElapsedColumn, + TimeRemainingColumn, + TaskProgressColumn, + MofNCompleteColumn, + SpinnerColumn, + TextColumn, +) from quick_algo import di_graph, pagerank diff --git a/src/plugins/knowledge/src/open_ie.py b/src/plugins/knowledge/src/open_ie.py index ea84af4a..75fd1854 100644 --- a/src/plugins/knowledge/src/open_ie.py +++ b/src/plugins/knowledge/src/open_ie.py @@ -154,7 +154,8 @@ class OpenIE: """提取原始段落""" raw_paragraph_dict = dict({doc_item["idx"]: doc_item["passage"] for doc_item in self.docs}) return raw_paragraph_dict - + + if __name__ == "__main__": # 测试代码 print(ROOT_PATH) From 81e5c1bb8bdc14b3e42b5fb84b1a49bb245409d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Fri, 2 May 2025 15:45:42 +0800 Subject: [PATCH 46/67] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BAOpenIE=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=AE=8C=E6=95=B4=E6=80=A7=E6=A3=80=E6=9F=A5=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=94=99=E8=AF=AF=E6=97=A5=E5=BF=97=E8=BE=93?= =?UTF-8?q?=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/import_openie.py | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/scripts/import_openie.py b/scripts/import_openie.py index dd4b50ec..2a6e09b7 100644 --- a/scripts/import_openie.py +++ b/scripts/import_openie.py @@ -6,6 +6,7 @@ import sys import os +from time import sleep sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) @@ -26,7 +27,7 @@ OPENIE_DIR = ( else os.path.join(ROOT_PATH, "data/openie") ) -logger = get_module_logger("LPMM知识库-OpenIE导入") +logger = get_module_logger("OpenIE导入") def hash_deduplicate( @@ -71,8 +72,45 @@ def handle_import_openie(openie_data: OpenIE, embed_manager: EmbeddingManager, k entity_list_data = openie_data.extract_entity_dict() # 索引的三元组列表 triple_list_data = openie_data.extract_triple_dict() + # print(openie_data.docs) if len(raw_paragraphs) != len(entity_list_data) or len(raw_paragraphs) != len(triple_list_data): logger.error("OpenIE数据存在异常") + logger.error(f"原始段落数量:{len(raw_paragraphs)}") + logger.error(f"实体列表数量:{len(entity_list_data)}") + logger.error(f"三元组列表数量:{len(triple_list_data)}") + logger.error("OpenIE数据段落数量与实体列表数量或三元组列表数量不一致") + logger.error("请保证你的原始数据分段良好,不要有类似于 “.....” 单独成一段的情况") + logger.error("或者一段中只有符号的情况") + # 新增:检查docs中每条数据的完整性 + logger.error("系统将于2秒后开始检查数据完整性") + sleep(2) + found_missing = False + for doc in getattr(openie_data, "docs", []): + idx = doc.get("idx", "<无idx>") + passage = doc.get("passage", "<无passage>") + missing = [] + # 检查字段是否存在且非空 + if "passage" not in doc or not doc.get("passage"): + missing.append("passage") + if "extracted_entities" not in doc or not isinstance(doc.get("extracted_entities"), list): + missing.append("名词列表缺失") + elif len(doc.get("extracted_entities", [])) == 0: + missing.append("名词列表为空") + if "extracted_triples" not in doc or not isinstance(doc.get("extracted_triples"), list): + missing.append("主谓宾三元组缺失") + elif len(doc.get("extracted_triples", [])) == 0: + missing.append("主谓宾三元组为空") + # 输出所有doc的idx + # print(f"检查: idx={idx}") + if missing: + found_missing = True + logger.error("\n") + logger.error("数据缺失:") + logger.error(f"对应哈希值:{idx}") + logger.error(f"对应文段内容内容:{passage}") + logger.error(f"非法原因:{', '.join(missing)}") + if not found_missing: + print("所有数据均完整,没有发现缺失字段。") return False # 将索引换为对应段落的hash值 logger.info("正在进行段落去重与重索引") From 931b2c3d2a57e9a3236207c0fa1841dd9a480294 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Fri, 2 May 2025 18:32:30 +0800 Subject: [PATCH 47/67] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E5=AD=98=E5=85=A5=E4=BF=A1=E6=81=AF=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/message_sender.py | 2 +- src/plugins/PFC/observation_info.py | 37 +++++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/plugins/PFC/message_sender.py b/src/plugins/PFC/message_sender.py index 53c20374..a33314e3 100644 --- a/src/plugins/PFC/message_sender.py +++ b/src/plugins/PFC/message_sender.py @@ -70,7 +70,7 @@ class DirectMessageSender: message_set = MessageSet(chat_stream, message_id) message_set.add_message(message) await message_manager.add_message(message_set) - await self.storage.store_message(message, chat_stream) + # await self.storage.store_message(message, chat_stream) logger.info(f"[私聊][{self.private_name}]PFC消息已发送: {content}") except Exception as e: diff --git a/src/plugins/PFC/observation_info.py b/src/plugins/PFC/observation_info.py index c7572955..5e0bdc16 100644 --- a/src/plugins/PFC/observation_info.py +++ b/src/plugins/PFC/observation_info.py @@ -6,6 +6,7 @@ from .chat_observer import ChatObserver from .chat_states import NotificationHandler, NotificationType, Notification from src.plugins.utils.chat_message_builder import build_readable_messages import traceback # 导入 traceback 用于调试 +from src.config.config import global_config logger = get_module_logger("observation_info") @@ -252,6 +253,27 @@ class ObservationInfo: message_time = message.get("time") message_id = message.get("message_id") processed_text = message.get("processed_plain_text", "") + is_bot_message = False + + # 确定发送者和是否为机器人 + if user_info: + sender_id = str(user_info.user_id) + if sender_id == str(global_config.BOT_QQ): + is_bot_message = True + # 更新机器人最后发言时间 + if message_time and message_time > (self.last_bot_speak_time or 0): + self.last_bot_speak_time = message_time + else: + # 更新用户最后发言时间 + if message_time and message_time > (self.last_user_speak_time or 0): + self.last_user_speak_time = message_time + self.active_users.add(sender_id) + self.last_message_sender = sender_id + else: + logger.warning( + f"[私聊][{self.private_name}]处理消息更新时缺少有效的 UserInfo 对象, message_id: {message_id}" + ) + self.last_message_sender = None # 只有在新消息到达时才更新 last_message 相关信息 if message_time and message_time > (self.last_message_time or 0): @@ -278,12 +300,17 @@ class ObservationInfo: ) self.last_message_sender = None # 发送者未知 - # 将原始消息字典添加到未处理列表 - self.unprocessed_messages.append(message) - self.new_messages_count = len(self.unprocessed_messages) # 直接用列表长度 + if not is_bot_message: + # 将原始消息字典添加到未处理列表 + self.unprocessed_messages.append(message) + self.new_messages_count = len(self.unprocessed_messages) # 直接用列表长度 + logger.debug(f"[私聊][{self.private_name}] 用户新消息加入未处理列表. 当前未处理数: {self.new_messages_count}") + # logger.debug(f"[私聊][{self.private_name}]消息更新: last_time={self.last_message_time}, new_count={self.new_messages_count}") + self.update_changed() # 标记状态已改变 + else: + # 是机器人自己的消息,仅记录日志,不加入未处理列表 + logger.debug(f"[私聊][{self.private_name}] 观察到机器人自身消息 (ID: {message_id}),仅更新时间戳,不处理。") - # logger.debug(f"[私聊][{self.private_name}]消息更新: last_time={self.last_message_time}, new_count={self.new_messages_count}") - self.update_changed() # 标记状态已改变 else: # 如果消息时间戳不是最新的,可能不需要处理,或者记录一个警告 pass From bea8ce81505ca64652ba6246aa53ada2942ab6dc Mon Sep 17 00:00:00 2001 From: Bakadax Date: Fri, 2 May 2025 18:34:59 +0800 Subject: [PATCH 48/67] ruff --- src/plugins/PFC/message_sender.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/PFC/message_sender.py b/src/plugins/PFC/message_sender.py index a33314e3..0c232ed8 100644 --- a/src/plugins/PFC/message_sender.py +++ b/src/plugins/PFC/message_sender.py @@ -6,7 +6,7 @@ from ..chat.message import Message from maim_message import UserInfo, Seg from src.plugins.chat.message import MessageSending, MessageSet from src.plugins.chat.message_sender import message_manager -from ..storage.storage import MessageStorage +# from ..storage.storage import MessageStorage from ...config.config import global_config @@ -18,7 +18,7 @@ class DirectMessageSender: def __init__(self, private_name: str): self.private_name = private_name - self.storage = MessageStorage() + # self.storage = MessageStorage() async def send_message( self, From 7fe55ddf689e6ba56b93e205905dc652cf62029a Mon Sep 17 00:00:00 2001 From: Bakadax Date: Fri, 2 May 2025 19:05:52 +0800 Subject: [PATCH 49/67] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=90=AF=E7=94=A8?= =?UTF-8?q?=E6=96=B0=E7=9A=84=20message=5Fsender=20=E5=90=8E=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=E9=BA=A6=E9=BA=A6=E6=B6=88=E6=81=AF=E8=A2=AB=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E5=AD=98=E5=85=A5=E6=95=B0=E6=8D=AE=E5=BA=93=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/action_planner.py | 12 +++++----- src/plugins/PFC/message_sender.py | 3 --- src/plugins/PFC/observation_info.py | 37 ++++------------------------- 3 files changed, 11 insertions(+), 41 deletions(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 1f10c973..ba16e8b0 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -514,13 +514,13 @@ class ActionPlanner: retrieved_knowledge_str_planner = await self._get_prompt_info(message=retrieval_context) logger.info(f"[私聊][{self.private_name}] (ActionPlanner) 自动检索知识 {'完成' if retrieved_knowledge_str_planner else '无结果'}。") except Exception as retrieval_err: - logger.error(f"[私聊][{self.private_name}] (ActionPlanner) 自动检索时出错: {retrieval_err}") - retrieved_memory_str_planner = "检索记忆时出错。\n" - retrieved_knowledge_str_planner = "检索知识时出错。\n" + logger.error(f"[私聊][{self.private_name}] (ActionPlanner) 自动检索时出错: {retrieval_err}") + retrieved_memory_str_planner = "检索记忆时出错。\n" + retrieved_knowledge_str_planner = "检索知识时出错。\n" else: - logger.debug(f"[私聊][{self.private_name}] (ActionPlanner) 无有效聊天记录,跳过自动检索。") - retrieved_memory_str_planner = "无聊天记录无法检索记忆。\n" - retrieved_knowledge_str_planner = "无聊天记录无法检索知识。\n" + logger.debug(f"[私聊][{self.private_name}] (ActionPlanner) 无有效聊天记录,跳过自动检索。") + retrieved_memory_str_planner = "无聊天记录无法检索记忆。\n" + retrieved_knowledge_str_planner = "无聊天记录无法检索知识。\n" # --- 选择 Prompt --- if last_successful_reply_action in ["direct_reply", "send_new_message"]: diff --git a/src/plugins/PFC/message_sender.py b/src/plugins/PFC/message_sender.py index 0c232ed8..abbafa9f 100644 --- a/src/plugins/PFC/message_sender.py +++ b/src/plugins/PFC/message_sender.py @@ -6,7 +6,6 @@ from ..chat.message import Message from maim_message import UserInfo, Seg from src.plugins.chat.message import MessageSending, MessageSet from src.plugins.chat.message_sender import message_manager -# from ..storage.storage import MessageStorage from ...config.config import global_config @@ -18,7 +17,6 @@ class DirectMessageSender: def __init__(self, private_name: str): self.private_name = private_name - # self.storage = MessageStorage() async def send_message( self, @@ -70,7 +68,6 @@ class DirectMessageSender: message_set = MessageSet(chat_stream, message_id) message_set.add_message(message) await message_manager.add_message(message_set) - # await self.storage.store_message(message, chat_stream) logger.info(f"[私聊][{self.private_name}]PFC消息已发送: {content}") except Exception as e: diff --git a/src/plugins/PFC/observation_info.py b/src/plugins/PFC/observation_info.py index 5e0bdc16..c7572955 100644 --- a/src/plugins/PFC/observation_info.py +++ b/src/plugins/PFC/observation_info.py @@ -6,7 +6,6 @@ from .chat_observer import ChatObserver from .chat_states import NotificationHandler, NotificationType, Notification from src.plugins.utils.chat_message_builder import build_readable_messages import traceback # 导入 traceback 用于调试 -from src.config.config import global_config logger = get_module_logger("observation_info") @@ -253,27 +252,6 @@ class ObservationInfo: message_time = message.get("time") message_id = message.get("message_id") processed_text = message.get("processed_plain_text", "") - is_bot_message = False - - # 确定发送者和是否为机器人 - if user_info: - sender_id = str(user_info.user_id) - if sender_id == str(global_config.BOT_QQ): - is_bot_message = True - # 更新机器人最后发言时间 - if message_time and message_time > (self.last_bot_speak_time or 0): - self.last_bot_speak_time = message_time - else: - # 更新用户最后发言时间 - if message_time and message_time > (self.last_user_speak_time or 0): - self.last_user_speak_time = message_time - self.active_users.add(sender_id) - self.last_message_sender = sender_id - else: - logger.warning( - f"[私聊][{self.private_name}]处理消息更新时缺少有效的 UserInfo 对象, message_id: {message_id}" - ) - self.last_message_sender = None # 只有在新消息到达时才更新 last_message 相关信息 if message_time and message_time > (self.last_message_time or 0): @@ -300,17 +278,12 @@ class ObservationInfo: ) self.last_message_sender = None # 发送者未知 - if not is_bot_message: - # 将原始消息字典添加到未处理列表 - self.unprocessed_messages.append(message) - self.new_messages_count = len(self.unprocessed_messages) # 直接用列表长度 - logger.debug(f"[私聊][{self.private_name}] 用户新消息加入未处理列表. 当前未处理数: {self.new_messages_count}") - # logger.debug(f"[私聊][{self.private_name}]消息更新: last_time={self.last_message_time}, new_count={self.new_messages_count}") - self.update_changed() # 标记状态已改变 - else: - # 是机器人自己的消息,仅记录日志,不加入未处理列表 - logger.debug(f"[私聊][{self.private_name}] 观察到机器人自身消息 (ID: {message_id}),仅更新时间戳,不处理。") + # 将原始消息字典添加到未处理列表 + self.unprocessed_messages.append(message) + self.new_messages_count = len(self.unprocessed_messages) # 直接用列表长度 + # logger.debug(f"[私聊][{self.private_name}]消息更新: last_time={self.last_message_time}, new_count={self.new_messages_count}") + self.update_changed() # 标记状态已改变 else: # 如果消息时间戳不是最新的,可能不需要处理,或者记录一个警告 pass From 5a7c54aceffaee1b7aef41970581e3e6f975d57a Mon Sep 17 00:00:00 2001 From: Bakadax Date: Fri, 2 May 2025 19:13:40 +0800 Subject: [PATCH 50/67] ruff --- src/plugins/PFC/action_planner.py | 8 ++++---- src/plugins/PFC/reply_generator.py | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index ba16e8b0..40333fc4 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -188,7 +188,7 @@ class ActionPlanner: final_results_content = [] for result in results: content = result.get("content", "").strip() - similarity = result.get("similarity", 0.0) + # similarity = result.get("similarity", 0.0) if content and content not in unique_contents: unique_contents.add(content) # 可以选择性地加入相似度信息,或者只加内容 @@ -213,8 +213,8 @@ class ActionPlanner: start_time = time.time() message = message.strip() if not message: - logger.debug(f"[私聊][{self.private_name}]自动知识检索:输入消息为空。") - return "" + logger.debug(f"[私聊][{self.private_name}]自动知识检索:输入消息为空。") + return "" logger.debug(f"[私聊][{self.private_name}]开始自动知识检索,消息: {message[:30]}...") @@ -712,7 +712,7 @@ def get_info_from_db( # 注意:这里的 logger 需要能访问到,或者在这个函数里获取 logger 实例 # logger.debug(f"旧知识库查询结果数量: {len(results)}") # 暂时注释掉,避免 logger 未定义 except Exception as e: - # logger.error(f"执行旧知识库聚合查询时出错: {e}") # 暂时注释掉 + logger.debug(f"执行旧知识库聚合查询时出错: {e}") results = [] if not results: diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index a9ed61fd..3599e6e1 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -179,7 +179,7 @@ class ReplyGenerator: final_results_content = [] for result in results: content = result.get("content", "").strip() - similarity = result.get("similarity", 0.0) + # similarity = result.get("similarity", 0.0) if content and content not in unique_contents: unique_contents.add(content) # 可以选择性地加入相似度信息,或者只加内容 @@ -204,8 +204,8 @@ class ReplyGenerator: start_time = time.time() message = message.strip() if not message: - logger.debug(f"[私聊][{self.private_name}]自动知识检索:输入消息为空。") - return "" + logger.debug(f"[私聊][{self.private_name}]自动知识检索:输入消息为空。") + return "" logger.debug(f"[私聊][{self.private_name}]开始自动知识检索,消息: {message[:30]}...") @@ -340,17 +340,17 @@ class ReplyGenerator: logger.debug(f"[私聊][{self.private_name}]开始自动检索记忆...") retrieved_memory_str = await self._get_memory_info(text=retrieval_context) if retrieved_memory_str: - logger.info(f"[私聊][{self.private_name}]自动检索到记忆片段。") + logger.info(f"[私聊][{self.private_name}]自动检索到记忆片段。") else: - logger.info(f"[私聊][{self.private_name}]未自动检索到相关记忆。") + logger.info(f"[私聊][{self.private_name}]未自动检索到相关记忆。") # 提取知识 logger.debug(f"[私聊][{self.private_name}]开始自动检索知识...") retrieved_knowledge_str = await self._get_prompt_info(message=retrieval_context) if retrieved_knowledge_str: - logger.info(f"[私聊][{self.private_name}]自动检索到相关知识。") + logger.info(f"[私聊][{self.private_name}]自动检索到相关知识。") else: - logger.info(f"[私聊][{self.private_name}]未自动检索到相关知识。") + logger.info(f"[私聊][{self.private_name}]未自动检索到相关知识。") except Exception as retrieval_err: logger.error(f"[私聊][{self.private_name}]在自动检索记忆/知识时发生错误: {retrieval_err}") @@ -470,7 +470,7 @@ def get_info_from_db( # 注意:这里的 logger 需要能访问到,或者在这个函数里获取 logger 实例 # logger.debug(f"旧知识库查询结果数量: {len(results)}") # 暂时注释掉,避免 logger 未定义 except Exception as e: - # logger.error(f"执行旧知识库聚合查询时出错: {e}") # 暂时注释掉 + logger.debug(f"执行旧知识库聚合查询时出错: {e}") results = [] if not results: From 3304dc6b290ad8d45be8bbc4113a2f7594a94685 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 2 May 2025 11:13:59 +0000 Subject: [PATCH 51/67] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/action_planner.py | 115 +++++++++++++++++------------ src/plugins/PFC/conversation.py | 46 ++++++------ src/plugins/PFC/reply_generator.py | 102 +++++++++++++++---------- 3 files changed, 154 insertions(+), 109 deletions(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 40333fc4..8a45bf13 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -4,6 +4,7 @@ from src.plugins.memory_system.Hippocampus import HippocampusManager from src.plugins.knowledge.knowledge_lib import qa_manager from src.common.database import db from src.plugins.chat.utils import get_embedding + # import jieba # 如果需要旧版知识库的回退,可能需要 # import re # 如果需要旧版知识库的回退,可能需要 from src.common.logger_manager import get_logger @@ -124,7 +125,7 @@ class ActionPlanner: self.name = global_config.BOT_NICKNAME self.private_name = private_name self.chat_observer = ChatObserver.get_instance(stream_id, private_name) - + async def _get_memory_info(self, text: str) -> str: """根据文本自动检索相关记忆""" memory_prompt = "" @@ -132,18 +133,20 @@ class ActionPlanner: try: related_memory = await HippocampusManager.get_instance().get_memory_from_text( text=text, - max_memory_num=2, # 最多获取 2 条记忆 - max_memory_length=2, # 每条记忆长度限制(这个参数含义可能需确认) - max_depth=3, # 搜索深度 - fast_retrieval=False # 是否快速检索 + max_memory_num=2, # 最多获取 2 条记忆 + max_memory_length=2, # 每条记忆长度限制(这个参数含义可能需确认) + max_depth=3, # 搜索深度 + fast_retrieval=False, # 是否快速检索 ) if related_memory: for memory in related_memory: # memory[0] 是记忆ID, memory[1] 是记忆内容 - related_memory_info += memory[1] + "\n" # 将记忆内容拼接起来 + related_memory_info += memory[1] + "\n" # 将记忆内容拼接起来 if related_memory_info: memory_prompt = f"你回忆起:\n{related_memory_info.strip()}\n(以上是你的回忆,供参考)\n" - logger.debug(f"[私聊]决策层[{self.private_name}]自动检索到记忆: {related_memory_info.strip()[:100]}...") + logger.debug( + f"[私聊]决策层[{self.private_name}]自动检索到记忆: {related_memory_info.strip()[:100]}..." + ) else: logger.debug(f"[私聊]决策层[{self.private_name}]自动检索记忆返回为空。") else: @@ -179,9 +182,11 @@ class ActionPlanner: return "" # 调用我们之前添加的 get_info_from_db 函数 - results = get_info_from_db(embedding, limit=5, threshold=threshold, return_raw=True) # 最多查 5 条 + results = get_info_from_db(embedding, limit=5, threshold=threshold, return_raw=True) # 最多查 5 条 - logger.info(f"[私聊][{self.private_name}]旧版知识库查询完成,耗时: {time.time() - start_time:.3f}秒,获取{len(results)}条结果") + logger.info( + f"[私聊][{self.private_name}]旧版知识库查询完成,耗时: {time.time() - start_time:.3f}秒,获取{len(results)}条结果" + ) # 去重和格式化 unique_contents = set() @@ -231,7 +236,9 @@ class ActionPlanner: else: logger.debug(f"[私聊][{self.private_name}]LPMM 知识库未返回有效知识,尝试旧版数据库检索。") except Exception as e: - logger.error(f"[私聊][{self.private_name}]调用 LPMM 知识库 (qa_manager.get_knowledge) 时发生异常: {str(e)},尝试旧版数据库检索。") + logger.error( + f"[私聊][{self.private_name}]调用 LPMM 知识库 (qa_manager.get_knowledge) 时发生异常: {str(e)},尝试旧版数据库检索。" + ) # 2. 如果 LPMM 失败或无结果,尝试旧版数据库 try: @@ -246,11 +253,15 @@ class ActionPlanner: logger.debug(f"[私聊][{self.private_name}]旧版数据库也未检索到有效知识。") except Exception as e2: - logger.error(f"[私聊][{self.private_name}]调用旧版知识库检索 (_get_prompt_info_old) 时也发生异常: {str(e2)}") + logger.error( + f"[私聊][{self.private_name}]调用旧版知识库检索 (_get_prompt_info_old) 时也发生异常: {str(e2)}" + ) # 如果两种方法都失败或无结果 - logger.info(f"[私聊][{self.private_name}]自动知识检索总耗时: {time.time() - start_time:.3f}秒,未找到任何相关知识。") - return "" # 返回空字符串 + logger.info( + f"[私聊][{self.private_name}]自动知识检索总耗时: {time.time() - start_time:.3f}秒,未找到任何相关知识。" + ) + return "" # 返回空字符串 # 修改 plan 方法签名,增加 last_successful_reply_action 参数 async def plan( @@ -362,36 +373,36 @@ class ActionPlanner: # --- 知识信息字符串构建开始 --- # knowledge_info_str = "【已获取的相关知识和记忆】\n" # try: - # 检查 conversation_info 是否有 knowledge_list 并且不为空 - # if hasattr(conversation_info, "knowledge_list") and conversation_info.knowledge_list: - # 最多只显示最近的 5 条知识,防止 Prompt 过长 - # recent_knowledge = conversation_info.knowledge_list[-5:] - # for i, knowledge_item in enumerate(recent_knowledge): - # if isinstance(knowledge_item, dict): - # query = knowledge_item.get("query", "未知查询") - # knowledge = knowledge_item.get("knowledge", "无知识内容") - # source = knowledge_item.get("source", "未知来源") - # 只取知识内容的前 2000 个字,避免太长 - # knowledge_snippet = knowledge[:2000] + "..." if len(knowledge) > 2000 else knowledge - # knowledge_info_str += ( - # f"{i + 1}. 关于 '{query}' 的知识 (来源: {source}):\n {knowledge_snippet}\n" - # ) - # else: - # 处理列表里不是字典的异常情况 - # knowledge_info_str += f"{i + 1}. 发现一条格式不正确的知识记录。\n" + # 检查 conversation_info 是否有 knowledge_list 并且不为空 + # if hasattr(conversation_info, "knowledge_list") and conversation_info.knowledge_list: + # 最多只显示最近的 5 条知识,防止 Prompt 过长 + # recent_knowledge = conversation_info.knowledge_list[-5:] + # for i, knowledge_item in enumerate(recent_knowledge): + # if isinstance(knowledge_item, dict): + # query = knowledge_item.get("query", "未知查询") + # knowledge = knowledge_item.get("knowledge", "无知识内容") + # source = knowledge_item.get("source", "未知来源") + # 只取知识内容的前 2000 个字,避免太长 + # knowledge_snippet = knowledge[:2000] + "..." if len(knowledge) > 2000 else knowledge + # knowledge_info_str += ( + # f"{i + 1}. 关于 '{query}' 的知识 (来源: {source}):\n {knowledge_snippet}\n" + # ) + # else: + # 处理列表里不是字典的异常情况 + # knowledge_info_str += f"{i + 1}. 发现一条格式不正确的知识记录。\n" - # if not recent_knowledge: # 如果 knowledge_list 存在但为空 - # knowledge_info_str += "- 暂无相关知识和记忆。\n" + # if not recent_knowledge: # 如果 knowledge_list 存在但为空 + # knowledge_info_str += "- 暂无相关知识和记忆。\n" - # else: - # 如果 conversation_info 没有 knowledge_list 属性,或者列表为空 - # knowledge_info_str += "- 暂无相关知识记忆。\n" + # else: + # 如果 conversation_info 没有 knowledge_list 属性,或者列表为空 + # knowledge_info_str += "- 暂无相关知识记忆。\n" # except AttributeError: - # logger.warning(f"[私聊][{self.private_name}]ConversationInfo 对象可能缺少 knowledge_list 属性。") - # knowledge_info_str += "- 获取知识列表时出错。\n" + # logger.warning(f"[私聊][{self.private_name}]ConversationInfo 对象可能缺少 knowledge_list 属性。") + # knowledge_info_str += "- 获取知识列表时出错。\n" # except Exception as e: - # logger.error(f"[私聊][{self.private_name}]构建知识信息字符串时出错: {e}") - # knowledge_info_str += "- 处理知识列表时出错。\n" + # logger.error(f"[私聊][{self.private_name}]构建知识信息字符串时出错: {e}") + # knowledge_info_str += "- 处理知识列表时出错。\n" # --- 知识信息字符串构建结束 --- # 获取聊天历史记录 (chat_history_text) @@ -503,16 +514,20 @@ class ActionPlanner: retrieved_memory_str_planner = "" retrieved_knowledge_str_planner = "" - retrieval_context = chat_history_text # 使用聊天记录作为检索上下文 + retrieval_context = chat_history_text # 使用聊天记录作为检索上下文 if retrieval_context and retrieval_context != "还没有聊天记录。" and retrieval_context != "[构建聊天记录出错]": try: logger.debug(f"[私聊][{self.private_name}] (ActionPlanner) 开始自动检索记忆...") retrieved_memory_str_planner = await self._get_memory_info(text=retrieval_context) - logger.info(f"[私聊][{self.private_name}] (ActionPlanner) 自动检索记忆 {'完成' if retrieved_memory_str_planner else '无结果'}。") + logger.info( + f"[私聊][{self.private_name}] (ActionPlanner) 自动检索记忆 {'完成' if retrieved_memory_str_planner else '无结果'}。" + ) logger.debug(f"[私聊][{self.private_name}] (ActionPlanner) 开始自动知识检索...") retrieved_knowledge_str_planner = await self._get_prompt_info(message=retrieval_context) - logger.info(f"[私聊][{self.private_name}] (ActionPlanner) 自动检索知识 {'完成' if retrieved_knowledge_str_planner else '无结果'}。") + logger.info( + f"[私聊][{self.private_name}] (ActionPlanner) 自动检索知识 {'完成' if retrieved_knowledge_str_planner else '无结果'}。" + ) except Exception as retrieval_err: logger.error(f"[私聊][{self.private_name}] (ActionPlanner) 自动检索时出错: {retrieval_err}") retrieved_memory_str_planner = "检索记忆时出错。\n" @@ -541,7 +556,9 @@ class ActionPlanner: chat_history_text=chat_history_text if chat_history_text.strip() else "还没有聊天记录。", # knowledge_info_str=knowledge_info_str, retrieved_memory_str=retrieved_memory_str_planner if retrieved_memory_str_planner else "无相关记忆。", - retrieved_knowledge_str=retrieved_knowledge_str_planner if retrieved_knowledge_str_planner else "无相关知识。" + retrieved_knowledge_str=retrieved_knowledge_str_planner + if retrieved_knowledge_str_planner + else "无相关知识。", ) logger.debug(f"[私聊][{self.private_name}]发送到LLM的最终提示词:\n------\n{prompt}\n------") @@ -644,7 +661,8 @@ class ActionPlanner: # 外层异常处理保持不变 logger.error(f"[私聊][{self.private_name}]规划行动时调用 LLM 或处理结果出错: {str(e)}") return "wait", f"行动规划处理中发生错误,暂时等待: {str(e)}" - + + def get_info_from_db( query_embedding: list, limit: int = 1, threshold: float = 0.5, return_raw: bool = False ) -> Union[str, list]: @@ -696,7 +714,13 @@ def get_info_from_db( } }, # 防止除以零错误,添加一个小的 epsilon - {"$addFields": {"similarity": {"$divide": ["$dotProduct", {"$max": [{"$multiply": ["$magnitude1", "$magnitude2"]}, 1e-9]}]}}}, + { + "$addFields": { + "similarity": { + "$divide": ["$dotProduct", {"$max": [{"$multiply": ["$magnitude1", "$magnitude2"]}, 1e-9]}] + } + } + }, { "$match": { "similarity": {"$gte": threshold} # 只保留相似度大于等于阈值的结果 @@ -723,4 +747,3 @@ def get_info_from_db( else: # 返回所有找到的内容,用换行分隔 return "\n".join(str(result["content"]) for result in results) - diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 2ecd6824..b2541ebb 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -506,30 +506,30 @@ class Conversation: conversation_info.done_action.append(wait_action_record) # elif action == "fetch_knowledge": - # self.state = ConversationState.FETCHING - # knowledge_query = reason - # try: - # 检查 knowledge_fetcher 是否存在 - # if not hasattr(self, "knowledge_fetcher"): - # logger.error(f"[私聊][{self.private_name}]KnowledgeFetcher 未初始化,无法获取知识。") - # raise AttributeError("KnowledgeFetcher not initialized") + # self.state = ConversationState.FETCHING + # knowledge_query = reason + # try: + # 检查 knowledge_fetcher 是否存在 + # if not hasattr(self, "knowledge_fetcher"): + # logger.error(f"[私聊][{self.private_name}]KnowledgeFetcher 未初始化,无法获取知识。") + # raise AttributeError("KnowledgeFetcher not initialized") - # knowledge, source = await self.knowledge_fetcher.fetch(knowledge_query, observation_info.chat_history) - # logger.info(f"[私聊][{self.private_name}]获取到知识: {knowledge[:100]}..., 来源: {source}") - # if knowledge: - # 确保 knowledge_list 存在 - # if not hasattr(conversation_info, "knowledge_list"): - # conversation_info.knowledge_list = [] - # conversation_info.knowledge_list.append( - # {"query": knowledge_query, "knowledge": knowledge, "source": source} - # ) - # action_successful = True - # except Exception as fetch_err: - # logger.error(f"[私聊][{self.private_name}]获取知识时出错: {str(fetch_err)}") - # conversation_info.done_action[action_index].update( - # {"status": "recall", "final_reason": f"获取知识失败: {str(fetch_err)}"} - # ) - # self.conversation_info.last_successful_reply_action = None # 重置状态 + # knowledge, source = await self.knowledge_fetcher.fetch(knowledge_query, observation_info.chat_history) + # logger.info(f"[私聊][{self.private_name}]获取到知识: {knowledge[:100]}..., 来源: {source}") + # if knowledge: + # 确保 knowledge_list 存在 + # if not hasattr(conversation_info, "knowledge_list"): + # conversation_info.knowledge_list = [] + # conversation_info.knowledge_list.append( + # {"query": knowledge_query, "knowledge": knowledge, "source": source} + # ) + # action_successful = True + # except Exception as fetch_err: + # logger.error(f"[私聊][{self.private_name}]获取知识时出错: {str(fetch_err)}") + # conversation_info.done_action[action_index].update( + # {"status": "recall", "final_reason": f"获取知识失败: {str(fetch_err)}"} + # ) + # self.conversation_info.last_successful_reply_action = None # 重置状态 elif action == "rethink_goal": self.state = ConversationState.RETHINKING diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index 3599e6e1..2c01cc0e 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -1,15 +1,19 @@ # 用于访问记忆系统 from src.plugins.memory_system.Hippocampus import HippocampusManager + # 用于访问新的知识库 (LPMM) from src.plugins.knowledge.knowledge_lib import qa_manager + # 用于访问数据库 (旧知识库需要) from src.common.database import db + # 用于获取文本的嵌入向量 (旧知识库需要) from src.plugins.chat.utils import get_embedding + # 可能用于旧知识库提取主题 (如果需要回退到旧方法) # import jieba # 如果报错说找不到 jieba,可能需要安装: pip install jieba # import re # 正则表达式库,通常 Python 自带 -from typing import Tuple, List, Dict, Any,Union +from typing import Tuple, List, Dict, Any, Union from src.common.logger import get_module_logger from ..models.utils_model import LLMRequest from ...config.config import global_config @@ -116,6 +120,7 @@ class ReplyGenerator: self.private_name = private_name self.chat_observer = ChatObserver.get_instance(stream_id, private_name) self.reply_checker = ReplyChecker(stream_id, private_name) + async def _get_memory_info(self, text: str) -> str: """根据文本自动检索相关记忆""" memory_prompt = "" @@ -123,15 +128,15 @@ class ReplyGenerator: try: related_memory = await HippocampusManager.get_instance().get_memory_from_text( text=text, - max_memory_num=2, # 最多获取 2 条记忆 - max_memory_length=2, # 每条记忆长度限制(这个参数含义可能需确认) - max_depth=3, # 搜索深度 - fast_retrieval=False # 是否快速检索 + max_memory_num=2, # 最多获取 2 条记忆 + max_memory_length=2, # 每条记忆长度限制(这个参数含义可能需确认) + max_depth=3, # 搜索深度 + fast_retrieval=False, # 是否快速检索 ) if related_memory: for memory in related_memory: # memory[0] 是记忆ID, memory[1] 是记忆内容 - related_memory_info += memory[1] + "\n" # 将记忆内容拼接起来 + related_memory_info += memory[1] + "\n" # 将记忆内容拼接起来 if related_memory_info: memory_prompt = f"你回忆起:\n{related_memory_info.strip()}\n(以上是你的回忆,不一定是目前聊天里的人说的,回忆中别人说的事情也不一定是准确的,请记住)\n" logger.debug(f"[私聊][{self.private_name}]自动检索到记忆: {related_memory_info.strip()[:100]}...") @@ -170,9 +175,11 @@ class ReplyGenerator: return "" # 调用我们之前添加的 get_info_from_db 函数 - results = get_info_from_db(embedding, limit=5, threshold=threshold, return_raw=True) # 最多查 5 条 + results = get_info_from_db(embedding, limit=5, threshold=threshold, return_raw=True) # 最多查 5 条 - logger.info(f"[私聊][{self.private_name}]旧版知识库查询完成,耗时: {time.time() - start_time:.3f}秒,获取{len(results)}条结果") + logger.info( + f"[私聊][{self.private_name}]旧版知识库查询完成,耗时: {time.time() - start_time:.3f}秒,获取{len(results)}条结果" + ) # 去重和格式化 unique_contents = set() @@ -222,7 +229,9 @@ class ReplyGenerator: else: logger.debug(f"[私聊][{self.private_name}]LPMM 知识库未返回有效知识,尝试旧版数据库检索。") except Exception as e: - logger.error(f"[私聊][{self.private_name}]调用 LPMM 知识库 (qa_manager.get_knowledge) 时发生异常: {str(e)},尝试旧版数据库检索。") + logger.error( + f"[私聊][{self.private_name}]调用 LPMM 知识库 (qa_manager.get_knowledge) 时发生异常: {str(e)},尝试旧版数据库检索。" + ) # 2. 如果 LPMM 失败或无结果,尝试旧版数据库 try: @@ -237,11 +246,15 @@ class ReplyGenerator: logger.debug(f"[私聊][{self.private_name}]旧版数据库也未检索到有效知识。") except Exception as e2: - logger.error(f"[私聊][{self.private_name}]调用旧版知识库检索 (_get_prompt_info_old) 时也发生异常: {str(e2)}") + logger.error( + f"[私聊][{self.private_name}]调用旧版知识库检索 (_get_prompt_info_old) 时也发生异常: {str(e2)}" + ) # 如果两种方法都失败或无结果 - logger.info(f"[私聊][{self.private_name}]自动知识检索总耗时: {time.time() - start_time:.3f}秒,未找到任何相关知识。") - return "" # 返回空字符串 - + logger.info( + f"[私聊][{self.private_name}]自动知识检索总耗时: {time.time() - start_time:.3f}秒,未找到任何相关知识。" + ) + return "" # 返回空字符串 + # 修改 generate 方法签名,增加 action_type 参数 async def generate( self, observation_info: ObservationInfo, conversation_info: ConversationInfo, action_type: str @@ -284,34 +297,34 @@ class ReplyGenerator: # --- 新增:构建知识信息字符串 --- # knowledge_info_str = "【供参考的相关知识和记忆】\n" # 稍微改下标题,表明是供参考 # try: - # 检查 conversation_info 是否有 knowledge_list 并且不为空 - # if hasattr(conversation_info, "knowledge_list") and conversation_info.knowledge_list: - # 最多只显示最近的 5 条知识 - # recent_knowledge = conversation_info.knowledge_list[-5:] - # for i, knowledge_item in enumerate(recent_knowledge): - # if isinstance(knowledge_item, dict): - # query = knowledge_item.get("query", "未知查询") - # knowledge = knowledge_item.get("knowledge", "无知识内容") - # source = knowledge_item.get("source", "未知来源") - # 只取知识内容的前 2000 个字 - # knowledge_snippet = knowledge[:2000] + "..." if len(knowledge) > 2000 else knowledge - # knowledge_info_str += ( - # f"{i + 1}. 关于 '{query}' (来源: {source}): {knowledge_snippet}\n" # 格式微调,更简洁 - # ) - # else: - # knowledge_info_str += f"{i + 1}. 发现一条格式不正确的知识记录。\n" + # 检查 conversation_info 是否有 knowledge_list 并且不为空 + # if hasattr(conversation_info, "knowledge_list") and conversation_info.knowledge_list: + # 最多只显示最近的 5 条知识 + # recent_knowledge = conversation_info.knowledge_list[-5:] + # for i, knowledge_item in enumerate(recent_knowledge): + # if isinstance(knowledge_item, dict): + # query = knowledge_item.get("query", "未知查询") + # knowledge = knowledge_item.get("knowledge", "无知识内容") + # source = knowledge_item.get("source", "未知来源") + # 只取知识内容的前 2000 个字 + # knowledge_snippet = knowledge[:2000] + "..." if len(knowledge) > 2000 else knowledge + # knowledge_info_str += ( + # f"{i + 1}. 关于 '{query}' (来源: {source}): {knowledge_snippet}\n" # 格式微调,更简洁 + # ) + # else: + # knowledge_info_str += f"{i + 1}. 发现一条格式不正确的知识记录。\n" - # if not recent_knowledge: - # knowledge_info_str += "- 暂无。\n" # 更简洁的提示 + # if not recent_knowledge: + # knowledge_info_str += "- 暂无。\n" # 更简洁的提示 - # else: - # knowledge_info_str += "- 暂无。\n" + # else: + # knowledge_info_str += "- 暂无。\n" # except AttributeError: - # logger.warning(f"[私聊][{self.private_name}]ConversationInfo 对象可能缺少 knowledge_list 属性。") - # knowledge_info_str += "- 获取知识列表时出错。\n" + # logger.warning(f"[私聊][{self.private_name}]ConversationInfo 对象可能缺少 knowledge_list 属性。") + # knowledge_info_str += "- 获取知识列表时出错。\n" # except Exception as e: - # logger.error(f"[私聊][{self.private_name}]构建知识信息字符串时出错: {e}") - # knowledge_info_str += "- 处理知识列表时出错。\n" + # logger.error(f"[私聊][{self.private_name}]构建知识信息字符串时出错: {e}") + # knowledge_info_str += "- 处理知识列表时出错。\n" # 获取聊天历史记录 (chat_history_text) chat_history_text = observation_info.chat_history_str @@ -378,8 +391,10 @@ class ReplyGenerator: goals_str=goals_str, chat_history_text=chat_history_text, # knowledge_info_str=knowledge_info_str, - retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", # 如果为空则提示无 - retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。" # 如果为空则提示无 + retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", # 如果为空则提示无 + retrieved_knowledge_str=retrieved_knowledge_str + if retrieved_knowledge_str + else "无相关知识。", # 如果为空则提示无 ) # --- 调用 LLM 生成 --- @@ -403,6 +418,7 @@ class ReplyGenerator: """ return await self.reply_checker.check(reply, goal, chat_history, chat_history_str, retry_count) + def get_info_from_db( query_embedding: list, limit: int = 1, threshold: float = 0.5, return_raw: bool = False ) -> Union[str, list]: @@ -454,7 +470,13 @@ def get_info_from_db( } }, # 防止除以零错误,添加一个小的 epsilon - {"$addFields": {"similarity": {"$divide": ["$dotProduct", {"$max": [{"$multiply": ["$magnitude1", "$magnitude2"]}, 1e-9]}]}}}, + { + "$addFields": { + "similarity": { + "$divide": ["$dotProduct", {"$max": [{"$multiply": ["$magnitude1", "$magnitude2"]}, 1e-9]}] + } + } + }, { "$match": { "similarity": {"$gte": threshold} # 只保留相似度大于等于阈值的结果 From 502f509630e4f073ff6b8d3db582d34cc2cd3ba3 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 2 May 2025 19:15:15 +0800 Subject: [PATCH 52/67] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E5=A4=8Drelationshi?= =?UTF-8?q?p=E5=8A=A0=E9=94=99=E4=BA=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/message_sender.py | 6 +++--- src/plugins/heartFC_chat/normal_chat.py | 5 ++++- src/plugins/person_info/relationship_manager.py | 15 +++++++-------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index 61e2dd49..c50c7aad 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -216,9 +216,9 @@ class MessageManager: # print(f"message.reply:{message.reply}") # --- 条件应用 set_reply 逻辑 --- - logger.debug( - f"[message.apply_set_reply_logic:{message.apply_set_reply_logic},message.is_head:{message.is_head},thinking_messages_count:{thinking_messages_count},thinking_messages_length:{thinking_messages_length},message.is_private_message():{message.is_private_message()}]" - ) + # logger.debug( + # f"[message.apply_set_reply_logic:{message.apply_set_reply_logic},message.is_head:{message.is_head},thinking_messages_count:{thinking_messages_count},thinking_messages_length:{thinking_messages_length},message.is_private_message():{message.is_private_message()}]" + # ) if ( message.apply_set_reply_logic # 检查标记 and message.is_head diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py index 70568f83..1c1372c5 100644 --- a/src/plugins/heartFC_chat/normal_chat.py +++ b/src/plugins/heartFC_chat/normal_chat.py @@ -178,8 +178,11 @@ class NormalChat: """更新关系情绪""" ori_response = ",".join(response_set) stance, emotion = await self.gpt._get_emotion_tags(ori_response, message.processed_plain_text) + user_info = message.message_info.user_info + platform = user_info.platform await relationship_manager.calculate_update_relationship_value( - chat_stream=self.chat_stream, + user_info, + platform, label=emotion, stance=stance, # 使用 self.chat_stream ) diff --git a/src/plugins/person_info/relationship_manager.py b/src/plugins/person_info/relationship_manager.py index fc8cf548..3c264b05 100644 --- a/src/plugins/person_info/relationship_manager.py +++ b/src/plugins/person_info/relationship_manager.py @@ -5,6 +5,7 @@ from bson.decimal128 import Decimal128 from .person_info import person_info_manager import time import random +from maim_message import UserInfo, Seg # import re # import traceback @@ -102,7 +103,7 @@ class RelationshipManager: # await person_info_manager.update_one_field(person_id, "user_avatar", user_avatar) await person_info_manager.qv_person_name(person_id, user_nickname, user_cardname, user_avatar) - async def calculate_update_relationship_value(self, chat_stream: ChatStream, label: str, stance: str) -> tuple: + async def calculate_update_relationship_value(self, user_info: UserInfo, platform: str, label: str, stance: str): """计算并变更关系值 新的关系值变更计算方式: 将关系值限定在-1000到1000 @@ -134,11 +135,11 @@ class RelationshipManager: "困惑": 0.5, } - person_id = person_info_manager.get_person_id(chat_stream.user_info.platform, chat_stream.user_info.user_id) + person_id = person_info_manager.get_person_id(platform, user_info.user_id) data = { - "platform": chat_stream.user_info.platform, - "user_id": chat_stream.user_info.user_id, - "nickname": chat_stream.user_info.user_nickname, + "platform": platform, + "user_id": user_info.user_id, + "nickname": user_info.user_nickname, "konw_time": int(time.time()), } old_value = await person_info_manager.get_value(person_id, "relationship_value") @@ -178,7 +179,7 @@ class RelationshipManager: level_num = self.calculate_level_num(old_value + value) relationship_level = ["厌恶", "冷漠", "一般", "友好", "喜欢", "暧昧"] logger.info( - f"用户: {chat_stream.user_info.user_nickname}" + f"用户: {user_info.user_nickname}" f"当前关系: {relationship_level[level_num]}, " f"关系值: {old_value:.2f}, " f"当前立场情感: {stance}-{label}, " @@ -187,8 +188,6 @@ class RelationshipManager: await person_info_manager.update_one_field(person_id, "relationship_value", old_value + value, data) - return chat_stream.user_info.user_nickname, value, relationship_level[level_num] - async def calculate_update_relationship_value_with_reason( self, chat_stream: ChatStream, label: str, stance: str, reason: str ) -> tuple: From a859f9238fa0df18023317276dcce3c1f0d1da23 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 2 May 2025 19:17:59 +0800 Subject: [PATCH 53/67] =?UTF-8?q?better=EF=BC=9A=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ![新版麦麦开始学习.bat | 65 +++++++++++++++++++++++--------- scripts/info_extraction.py | 4 +- scripts/raw_data_preprocessor.py | 2 +- 3 files changed, 51 insertions(+), 20 deletions(-) diff --git a/![新版麦麦开始学习.bat b/![新版麦麦开始学习.bat index 41fc9836..b95bad00 100644 --- a/![新版麦麦开始学习.bat +++ b/![新版麦麦开始学习.bat @@ -2,26 +2,57 @@ CHCP 65001 > nul setlocal enabledelayedexpansion -REM 查找venv虚拟环境 -set "venv_path=%~dp0venv\Scripts\activate.bat" -if not exist "%venv_path%" ( - echo 错误: 未找到虚拟环境,请确保venv目录存在 - pause - exit /b 1 +echo 你需要选择启动方式,输入字母来选择: +echo V = 不知道什么意思就输入 V +echo C = 输入 C 使用 Conda 环境 +echo. +choice /C CV /N /M "在下方输入字母并回车 (C/V)?" /T 10 /D V + +set "ENV_TYPE=" +if %ERRORLEVEL% == 1 set "ENV_TYPE=CONDA" +if %ERRORLEVEL% == 2 set "ENV_TYPE=VENV" + +if "%ENV_TYPE%" == "CONDA" ( + set /p CONDA_ENV_NAME="请输入要使用的 Conda 环境名称: " + if not defined CONDA_ENV_NAME ( + echo 错误: 未输入 Conda 环境名称. + pause + exit /b 1 + ) + echo 选择: Conda '%CONDA_ENV_NAME%' + REM 激活Conda环境 + call conda activate %CONDA_ENV_NAME% + if %ERRORLEVEL% neq 0 ( + echo 错误: Conda环境 '%CONDA_ENV_NAME%' 激活失败. 请确保Conda已安装并正确配置, 且 '%CONDA_ENV_NAME%' 环境存在. + pause + exit /b 1 + ) +) else ( + echo Selected: venv (default) + REM 查找venv虚拟环境 + set "venv_path=%~dp0venv\Scripts\activate.bat" + if not exist "%venv_path%" ( + echo Error: venv not found. Ensure the venv directory exists alongside the script. + pause + exit /b 1 + ) + REM 激活虚拟环境 + call "%venv_path%" + if %ERRORLEVEL% neq 0 ( + echo Error: Failed to activate venv virtual environment. + pause + exit /b 1 + ) ) -REM 激活虚拟环境 -call "%venv_path%" -if %ERRORLEVEL% neq 0 ( - echo 错误: 虚拟环境激活失败 - pause - exit /b 1 -) +echo Environment activated successfully! + +REM --- 后续脚本执行 --- REM 运行预处理脚本 python "%~dp0scripts\raw_data_preprocessor.py" if %ERRORLEVEL% neq 0 ( - echo 错误: raw_data_preprocessor.py 执行失败 + echo Error: raw_data_preprocessor.py execution failed. pause exit /b 1 ) @@ -29,7 +60,7 @@ if %ERRORLEVEL% neq 0 ( REM 运行信息提取脚本 python "%~dp0scripts\info_extraction.py" if %ERRORLEVEL% neq 0 ( - echo 错误: info_extraction.py 执行失败 + echo Error: info_extraction.py execution failed. pause exit /b 1 ) @@ -37,10 +68,10 @@ if %ERRORLEVEL% neq 0 ( REM 运行OpenIE导入脚本 python "%~dp0scripts\import_openie.py" if %ERRORLEVEL% neq 0 ( - echo 错误: import_openie.py 执行失败 + echo Error: import_openie.py execution failed. pause exit /b 1 ) -echo 所有处理步骤完成! +echo All processing steps completed! pause \ No newline at end of file diff --git a/scripts/info_extraction.py b/scripts/info_extraction.py index 65c4082b..9e079070 100644 --- a/scripts/info_extraction.py +++ b/scripts/info_extraction.py @@ -87,8 +87,8 @@ def main(): signal.signal(signal.SIGINT, signal_handler) # 新增用户确认提示 - print("=== 重要操作确认 ===") - print("实体提取操作将会花费较多资金和时间,建议在空闲时段执行。") + print("=== 重要操作确认,请认真阅读以下内容哦 ===") + print("实体提取操作将会花费较多api余额和时间,建议在空闲时段执行。") print("举例:600万字全剧情,提取选用deepseek v3 0324,消耗约40元,约3小时。") print("建议使用硅基流动的非Pro模型") print("或者使用可以用赠金抵扣的Pro模型") diff --git a/scripts/raw_data_preprocessor.py b/scripts/raw_data_preprocessor.py index 2fc30352..33d51153 100644 --- a/scripts/raw_data_preprocessor.py +++ b/scripts/raw_data_preprocessor.py @@ -44,7 +44,7 @@ def process_text_file(file_path): def main(): # 新增用户确认提示 - print("=== 重要操作确认 ===") + print("=== 重要操作确认,请认真阅读以下内容哦 ===") print("如果你并非第一次导入知识") print("请先删除data/import.json文件,备份data/openie.json文件") print("在进行知识库导入之前") From 021ac90ead332195eb69d2cf98e556a6bcc7c3a3 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 2 May 2025 19:46:04 +0800 Subject: [PATCH 54/67] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E6=94=B9bat?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E6=8F=8F=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ![新版麦麦开始学习.bat | 29 ++++++++++++++++++++--------- scripts/raw_data_preprocessor.py | 9 ++++----- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/![新版麦麦开始学习.bat b/![新版麦麦开始学习.bat index b95bad00..eacaa2eb 100644 --- a/![新版麦麦开始学习.bat +++ b/![新版麦麦开始学习.bat @@ -6,29 +6,39 @@ echo 你需要选择启动方式,输入字母来选择: echo V = 不知道什么意思就输入 V echo C = 输入 C 使用 Conda 环境 echo. -choice /C CV /N /M "在下方输入字母并回车 (C/V)?" /T 10 /D V +choice /C CV /N /M "不知道什么意思就输入 V (C/V)?" /T 10 /D V set "ENV_TYPE=" if %ERRORLEVEL% == 1 set "ENV_TYPE=CONDA" if %ERRORLEVEL% == 2 set "ENV_TYPE=VENV" -if "%ENV_TYPE%" == "CONDA" ( +if "%ENV_TYPE%" == "CONDA" goto activate_conda +if "%ENV_TYPE%" == "VENV" goto activate_venv + +REM 如果 choice 超时或返回意外值,默认使用 venv +echo WARN: Invalid selection or timeout from choice. Defaulting to VENV. +set "ENV_TYPE=VENV" +goto activate_venv + +:activate_conda set /p CONDA_ENV_NAME="请输入要使用的 Conda 环境名称: " if not defined CONDA_ENV_NAME ( echo 错误: 未输入 Conda 环境名称. pause exit /b 1 ) - echo 选择: Conda '%CONDA_ENV_NAME%' + echo 选择: Conda '!CONDA_ENV_NAME!' REM 激活Conda环境 - call conda activate %CONDA_ENV_NAME% - if %ERRORLEVEL% neq 0 ( - echo 错误: Conda环境 '%CONDA_ENV_NAME%' 激活失败. 请确保Conda已安装并正确配置, 且 '%CONDA_ENV_NAME%' 环境存在. + call conda activate !CONDA_ENV_NAME! + if !ERRORLEVEL! neq 0 ( + echo 错误: Conda环境 '!CONDA_ENV_NAME!' 激活失败. 请确保Conda已安装并正确配置, 且 '!CONDA_ENV_NAME!' 环境存在. pause exit /b 1 ) -) else ( - echo Selected: venv (default) + goto env_activated + +:activate_venv + echo Selected: venv (default or selected) REM 查找venv虚拟环境 set "venv_path=%~dp0venv\Scripts\activate.bat" if not exist "%venv_path%" ( @@ -43,8 +53,9 @@ if "%ENV_TYPE%" == "CONDA" ( pause exit /b 1 ) -) + goto env_activated +:env_activated echo Environment activated successfully! REM --- 后续脚本执行 --- diff --git a/scripts/raw_data_preprocessor.py b/scripts/raw_data_preprocessor.py index 056cf572..c87c30ca 100644 --- a/scripts/raw_data_preprocessor.py +++ b/scripts/raw_data_preprocessor.py @@ -48,11 +48,10 @@ def process_text_file(file_path): def main(): # 新增用户确认提示 - print("=== 重要操作确认,请认真阅读以下内容哦 ===") - print("如果你并非第一次导入知识") - print("请先删除data/import.json文件,备份data/openie.json文件") - print("在进行知识库导入之前") - print("请修改config/lpmm_config.toml中的配置项") + print("=== 数据预处理脚本 ===") + print(f"本脚本将处理 '{RAW_DATA_PATH}' 目录下的所有 .txt 文件。") + print(f"处理后的段落数据将合并,并以 MM-DD-HH-SS-imported-data.json 的格式保存在 '{IMPORTED_DATA_PATH}' 目录中。") + print("请确保原始数据已放置在正确的目录中。") confirm = input("确认继续执行?(y/n): ").strip().lower() if confirm != "y": logger.error("操作已取消") From acbf5c974bfb173f216eb4cfe681dd0748c95342 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Fri, 2 May 2025 20:38:34 +0800 Subject: [PATCH 55/67] =?UTF-8?q?=E8=A1=A8=E6=83=85=E5=8C=85=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/emoji_system/emoji_manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plugins/emoji_system/emoji_manager.py b/src/plugins/emoji_system/emoji_manager.py index 86dab9d9..d105e0b8 100644 --- a/src/plugins/emoji_system/emoji_manager.py +++ b/src/plugins/emoji_system/emoji_manager.py @@ -289,7 +289,6 @@ def _to_emoji_objects(data): except Exception as e: logger.error(f"[加载错误] 处理数据库记录时出错 ({full_path}): {str(e)}") load_errors += 1 - return emoji_objects, load_errors return emoji_objects, load_errors From 6747e1d44177e419ecde9a51a41380fdcd6543d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Fri, 2 May 2025 20:42:58 +0800 Subject: [PATCH 56/67] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=A8=A1=E6=9D=BF=EF=BC=8C=E6=B7=BB=E5=8A=A0lpmm?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E4=BF=A1=E6=81=AF=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E6=97=A5=E5=BF=97=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/knowledge/src/lpmmconfig.py | 24 ++++++++++-------------- template/lpmm_config_template.toml | 3 +++ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/plugins/knowledge/src/lpmmconfig.py b/src/plugins/knowledge/src/lpmmconfig.py index 753562f4..040bdedc 100644 --- a/src/plugins/knowledge/src/lpmmconfig.py +++ b/src/plugins/knowledge/src/lpmmconfig.py @@ -1,7 +1,7 @@ import os import toml import sys -import argparse +# import argparse from .global_logger import logger PG_NAMESPACE = "paragraph" @@ -37,7 +37,8 @@ def _load_config(config, config_file_path): # Check if all top-level keys from default config exist in the file config for key in config.keys(): if key not in file_config: - print(f"警告: 配置文件 '{config_file_path}' 缺少必需的顶级键: '{key}'。请检查配置文件。") + logger.critical(f"警告: 配置文件 '{config_file_path}' 缺少必需的顶级键: '{key}'。请检查配置文件。") + logger.critical("请通过template/lpmm_config_template.toml文件进行更新") sys.exit(1) if "llm_providers" in file_config: @@ -68,16 +69,11 @@ def _load_config(config, config_file_path): logger.info(f"从文件中读取配置: {config_file_path}") -parser = argparse.ArgumentParser(description="Configurations for the pipeline") -parser.add_argument( - "--config_path", - type=str, - default="lpmm_config.toml", - help="Path to the configuration file", -) - global_config = dict( { + "lpmm":{ + "version": "0.1.0", + }, "llm_providers": { "localhost": { "base_url": "https://api.siliconflow.cn/v1", @@ -136,8 +132,8 @@ global_config = dict( ) # _load_config(global_config, parser.parse_args().config_path) -file_path = os.path.abspath(__file__) -dir_path = os.path.dirname(file_path) -root_path = os.path.join(dir_path, os.pardir, os.pardir, os.pardir, os.pardir) -config_path = os.path.join(root_path, "config", "lpmm_config.toml") +# file_path = os.path.abspath(__file__) +# dir_path = os.path.dirname(file_path) +ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) +config_path = os.path.join(ROOT_PATH, "config", "lpmm_config.toml") _load_config(global_config, config_path) diff --git a/template/lpmm_config_template.toml b/template/lpmm_config_template.toml index 8563b7ca..aae664d5 100644 --- a/template/lpmm_config_template.toml +++ b/template/lpmm_config_template.toml @@ -1,3 +1,6 @@ +[lpmm] +version = "0.1.0" + # LLM API 服务提供商,可配置多个 [[llm_providers]] name = "localhost" From 5d1c880fb9e99f7a8a7d78ecbc6242205b46b971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Fri, 2 May 2025 20:45:06 +0800 Subject: [PATCH 57/67] fix: Ruff --- src/plugins/person_info/relationship_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/person_info/relationship_manager.py b/src/plugins/person_info/relationship_manager.py index 3c264b05..862f2398 100644 --- a/src/plugins/person_info/relationship_manager.py +++ b/src/plugins/person_info/relationship_manager.py @@ -5,7 +5,7 @@ from bson.decimal128 import Decimal128 from .person_info import person_info_manager import time import random -from maim_message import UserInfo, Seg +from maim_message import UserInfo # import re # import traceback From 4f9fbe78728adc2d2afd1f815de3f958901536e7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 2 May 2025 12:45:19 +0000 Subject: [PATCH 58/67] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/knowledge/src/lpmmconfig.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/knowledge/src/lpmmconfig.py b/src/plugins/knowledge/src/lpmmconfig.py index 040bdedc..387a7b29 100644 --- a/src/plugins/knowledge/src/lpmmconfig.py +++ b/src/plugins/knowledge/src/lpmmconfig.py @@ -1,6 +1,7 @@ import os import toml import sys + # import argparse from .global_logger import logger @@ -71,7 +72,7 @@ def _load_config(config, config_file_path): global_config = dict( { - "lpmm":{ + "lpmm": { "version": "0.1.0", }, "llm_providers": { From 00851c3d8f8b373e97c2f26d4b9f3d34baecf72e Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Fri, 2 May 2025 23:19:47 +0800 Subject: [PATCH 59/67] =?UTF-8?q?=E7=AE=80=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/action_planner.py | 242 +++-------------------------- src/plugins/PFC/reply_generator.py | 232 +++------------------------ 2 files changed, 41 insertions(+), 433 deletions(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 40333fc4..d9bb672d 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -4,6 +4,10 @@ from src.plugins.memory_system.Hippocampus import HippocampusManager from src.plugins.knowledge.knowledge_lib import qa_manager from src.common.database import db from src.plugins.chat.utils import get_embedding +# --- NEW IMPORT --- +# 从 heartflow 导入知识检索和数据库查询函数/实例 +from src.plugins.heartFC_chat.heartflow_prompt_builder import prompt_builder, get_info_from_db +# --- END NEW IMPORT --- # import jieba # 如果需要旧版知识库的回退,可能需要 # import re # 如果需要旧版知识库的回退,可能需要 from src.common.logger_manager import get_logger @@ -34,7 +38,7 @@ PROMPT_INITIAL_REPLY = """{persona_text}。现在你在参与一场QQ私聊, 【上一次行动的详细情况和结果】 {last_action_context} 【时间和超时提示】 -{time_since_last_bot_message_info}{timeout_context} +{time_since_last_bot_message_info}{timeout_context} 【最近的对话记录】(包括你已成功发送的消息 和 新收到的消息) {chat_history_text} 【你的的回忆】 @@ -57,7 +61,7 @@ block_and_ignore: 更加极端的结束对话方式,直接结束对话并在 注意:请严格按照JSON格式输出,不要包含任何其他内容。""" # Prompt(2): 上一次成功回复后,决定继续发言时的决策 Prompt -PROMPT_FOLLOW_UP = """{persona_text}。现在你在参与一场QQ私聊,刚刚你已经回复了对方,请根据以下【所有信息】审慎且灵活的决策下一步行动,可以继续发送新消息,可以等待,可以倾听,可以调取知识,甚至可以屏蔽对方: +PROMPT_FOLLOW_UP = """{persona_text}。现在你在参与一场QQ私聊,刚刚你已经回复了对方,请根据以下【所有信息】审慎且灵活的决策下一步行动,可以继续发送新消息,可以等待,可以倾听,可以调取知识,甚至可以屏蔽对方: 【当前对话目标】 {goals_str} @@ -68,7 +72,7 @@ PROMPT_FOLLOW_UP = """{persona_text}。现在你在参与一场QQ私聊,刚刚 【上一次行动的详细情况和结果】 {last_action_context} 【时间和超时提示】 -{time_since_last_bot_message_info}{timeout_context} +{time_since_last_bot_message_info}{timeout_context} 【最近的对话记录】(包括你已成功发送的消息 和 新收到的消息) {chat_history_text} 【你的的回忆】 @@ -124,7 +128,8 @@ class ActionPlanner: self.name = global_config.BOT_NICKNAME self.private_name = private_name self.chat_observer = ChatObserver.get_instance(stream_id, private_name) - + + # _get_memory_info 保持不变 async def _get_memory_info(self, text: str) -> str: """根据文本自动检索相关记忆""" memory_prompt = "" @@ -153,104 +158,9 @@ class ActionPlanner: # memory_prompt = "检索记忆时出错。\n" # 可以选择是否提示错误 return memory_prompt - async def _get_prompt_info_old(self, message: str, threshold: float) -> str: - """ - 旧版的知识检索方法,根据消息文本从旧知识库(knowledges collection)检索。 - (移植并自 heartflow_prompt_builder.py) - """ - related_info = "" - start_time = time.time() - logger.debug(f"[私聊]决策层[{self.private_name}]开始使用旧版知识检索,消息: {message[:30]}...") + # --- REMOVED _get_prompt_info_old --- - # 简化处理:直接使用整个消息进行查询,不再提取主题 - query_text = message.strip() - if not query_text: - logger.debug(f"[私聊]决策层[{self.private_name}]旧版知识检索:消息为空,跳过。") - return "" - - embedding = None - try: - embedding = await get_embedding(query_text, request_type="pfc_implicit_knowledge") - except Exception as e: - logger.error(f"[私聊]决策层[{self.private_name}]旧版知识检索:获取嵌入向量时出错: {str(e)}") - - if not embedding: - logger.error(f"[私聊]决策层[{self.private_name}]旧版知识检索:获取嵌入向量失败。") - return "" - - # 调用我们之前添加的 get_info_from_db 函数 - results = get_info_from_db(embedding, limit=5, threshold=threshold, return_raw=True) # 最多查 5 条 - - logger.info(f"[私聊][{self.private_name}]旧版知识库查询完成,耗时: {time.time() - start_time:.3f}秒,获取{len(results)}条结果") - - # 去重和格式化 - unique_contents = set() - final_results_content = [] - for result in results: - content = result.get("content", "").strip() - # similarity = result.get("similarity", 0.0) - if content and content not in unique_contents: - unique_contents.add(content) - # 可以选择性地加入相似度信息,或者只加内容 - # final_results_content.append(f"[{similarity:.2f}] {content}") - final_results_content.append(content) - - if final_results_content: - related_info = "\n".join(final_results_content) - logger.debug(f"[私聊][{self.private_name}]旧版知识检索格式化后内容: {related_info[:100]}...") - else: - logger.debug(f"[私聊][{self.private_name}]旧版知识检索未找到合适结果或结果为空。") - - logger.info(f"[私聊][{self.private_name}]旧版知识检索总耗时: {time.time() - start_time:.3f}秒") - return related_info - - async def _get_prompt_info(self, message: str, threshold: float = 0.38) -> str: - """ - 自动检索相关知识的主函数。优先使用 LPMM,失败则回退到旧版。 - (移植自 heartflow_prompt_builder.py) - """ - related_info = "" - start_time = time.time() - message = message.strip() - if not message: - logger.debug(f"[私聊][{self.private_name}]自动知识检索:输入消息为空。") - return "" - - logger.debug(f"[私聊][{self.private_name}]开始自动知识检索,消息: {message[:30]}...") - - # 1. 尝试从 LPMM 知识库获取知识 - try: - found_knowledge_from_lpmm = qa_manager.get_knowledge(message) - if found_knowledge_from_lpmm and found_knowledge_from_lpmm.strip(): - related_info = found_knowledge_from_lpmm.strip() - logger.info(f"[私聊][{self.private_name}]从 LPMM 知识库获取到知识,长度: {len(related_info)}") - logger.debug(f"[私聊][{self.private_name}]LPMM 知识内容: {related_info[:100]}...") - # LPMM 成功获取,直接返回 - logger.info(f"[私聊][{self.private_name}]自动知识检索(LPMM)耗时: {time.time() - start_time:.3f}秒") - return related_info - else: - logger.debug(f"[私聊][{self.private_name}]LPMM 知识库未返回有效知识,尝试旧版数据库检索。") - except Exception as e: - logger.error(f"[私聊][{self.private_name}]调用 LPMM 知识库 (qa_manager.get_knowledge) 时发生异常: {str(e)},尝试旧版数据库检索。") - - # 2. 如果 LPMM 失败或无结果,尝试旧版数据库 - try: - knowledge_from_old = await self._get_prompt_info_old(message, threshold=threshold) - if knowledge_from_old and knowledge_from_old.strip(): - related_info = knowledge_from_old.strip() - logger.info(f"[私聊][{self.private_name}]从旧版数据库检索到知识,长度: {len(related_info)}") - # 旧版成功获取,返回 - logger.info(f"[私聊][{self.private_name}]自动知识检索(旧版)耗时: {time.time() - start_time:.3f}秒") - return related_info - else: - logger.debug(f"[私聊][{self.private_name}]旧版数据库也未检索到有效知识。") - - except Exception as e2: - logger.error(f"[私聊][{self.private_name}]调用旧版知识库检索 (_get_prompt_info_old) 时也发生异常: {str(e2)}") - - # 如果两种方法都失败或无结果 - logger.info(f"[私聊][{self.private_name}]自动知识检索总耗时: {time.time() - start_time:.3f}秒,未找到任何相关知识。") - return "" # 返回空字符串 + # --- REMOVED _get_prompt_info --- # 修改 plan 方法签名,增加 last_successful_reply_action 参数 async def plan( @@ -359,40 +269,6 @@ class ActionPlanner: logger.error(f"[私聊][{self.private_name}]构建对话目标字符串时出错: {e}") goals_str = "- 构建对话目标时出错。\n" - # --- 知识信息字符串构建开始 --- - # knowledge_info_str = "【已获取的相关知识和记忆】\n" - # try: - # 检查 conversation_info 是否有 knowledge_list 并且不为空 - # if hasattr(conversation_info, "knowledge_list") and conversation_info.knowledge_list: - # 最多只显示最近的 5 条知识,防止 Prompt 过长 - # recent_knowledge = conversation_info.knowledge_list[-5:] - # for i, knowledge_item in enumerate(recent_knowledge): - # if isinstance(knowledge_item, dict): - # query = knowledge_item.get("query", "未知查询") - # knowledge = knowledge_item.get("knowledge", "无知识内容") - # source = knowledge_item.get("source", "未知来源") - # 只取知识内容的前 2000 个字,避免太长 - # knowledge_snippet = knowledge[:2000] + "..." if len(knowledge) > 2000 else knowledge - # knowledge_info_str += ( - # f"{i + 1}. 关于 '{query}' 的知识 (来源: {source}):\n {knowledge_snippet}\n" - # ) - # else: - # 处理列表里不是字典的异常情况 - # knowledge_info_str += f"{i + 1}. 发现一条格式不正确的知识记录。\n" - - # if not recent_knowledge: # 如果 knowledge_list 存在但为空 - # knowledge_info_str += "- 暂无相关知识和记忆。\n" - - # else: - # 如果 conversation_info 没有 knowledge_list 属性,或者列表为空 - # knowledge_info_str += "- 暂无相关知识记忆。\n" - # except AttributeError: - # logger.warning(f"[私聊][{self.private_name}]ConversationInfo 对象可能缺少 knowledge_list 属性。") - # knowledge_info_str += "- 获取知识列表时出错。\n" - # except Exception as e: - # logger.error(f"[私聊][{self.private_name}]构建知识信息字符串时出错: {e}") - # knowledge_info_str += "- 处理知识列表时出错。\n" - # --- 知识信息字符串构建结束 --- # 获取聊天历史记录 (chat_history_text) try: @@ -501,18 +377,24 @@ class ActionPlanner: last_action_context += f"- 该行动当前状态: {status}\n" # self.last_successful_action_type = None # 非完成状态,清除记录 - retrieved_memory_str_planner = "" + retrieved_memory_str_planner = "" retrieved_knowledge_str_planner = "" retrieval_context = chat_history_text # 使用聊天记录作为检索上下文 if retrieval_context and retrieval_context != "还没有聊天记录。" and retrieval_context != "[构建聊天记录出错]": try: + # 调用本地的 _get_memory_info logger.debug(f"[私聊][{self.private_name}] (ActionPlanner) 开始自动检索记忆...") retrieved_memory_str_planner = await self._get_memory_info(text=retrieval_context) logger.info(f"[私聊][{self.private_name}] (ActionPlanner) 自动检索记忆 {'完成' if retrieved_memory_str_planner else '无结果'}。") - logger.debug(f"[私聊][{self.private_name}] (ActionPlanner) 开始自动知识检索...") - retrieved_knowledge_str_planner = await self._get_prompt_info(message=retrieval_context) + # --- MODIFIED KNOWLEDGE RETRIEVAL --- + # 调用导入的 prompt_builder.get_prompt_info + logger.debug(f"[私聊][{self.private_name}] (ActionPlanner) 开始自动检索知识 (使用导入函数)...") + # 使用导入的 prompt_builder 实例及其方法 + retrieved_knowledge_str_planner = await prompt_builder.get_prompt_info(message=retrieval_context, threshold=0.38) + # --- END MODIFIED KNOWLEDGE RETRIEVAL --- logger.info(f"[私聊][{self.private_name}] (ActionPlanner) 自动检索知识 {'完成' if retrieved_knowledge_str_planner else '无结果'}。") + except Exception as retrieval_err: logger.error(f"[私聊][{self.private_name}] (ActionPlanner) 自动检索时出错: {retrieval_err}") retrieved_memory_str_planner = "检索记忆时出错。\n" @@ -539,7 +421,7 @@ class ActionPlanner: time_since_last_bot_message_info=time_since_last_bot_message_info, timeout_context=timeout_context, chat_history_text=chat_history_text if chat_history_text.strip() else "还没有聊天记录。", - # knowledge_info_str=knowledge_info_str, + # knowledge_info_str=knowledge_info_str, # 移除了旧知识展示方式 retrieved_memory_str=retrieved_memory_str_planner if retrieved_memory_str_planner else "无相关记忆。", retrieved_knowledge_str=retrieved_knowledge_str_planner if retrieved_knowledge_str_planner else "无相关知识。" ) @@ -643,84 +525,4 @@ class ActionPlanner: except Exception as e: # 外层异常处理保持不变 logger.error(f"[私聊][{self.private_name}]规划行动时调用 LLM 或处理结果出错: {str(e)}") - return "wait", f"行动规划处理中发生错误,暂时等待: {str(e)}" - -def get_info_from_db( - query_embedding: list, limit: int = 1, threshold: float = 0.5, return_raw: bool = False -) -> Union[str, list]: - """ - 从旧知识库 (knowledges collection) 中根据嵌入向量相似度检索信息。 - (移植自 heartflow_prompt_builder.py) - """ - if not query_embedding: - return "" if not return_raw else [] - # 使用余弦相似度计算 - pipeline = [ - { - "$addFields": { - "dotProduct": { - "$reduce": { - "input": {"$range": [0, {"$size": "$embedding"}]}, - "initialValue": 0, - "in": { - "$add": [ - "$$value", - { - "$multiply": [ - {"$arrayElemAt": ["$embedding", "$$this"]}, - {"$arrayElemAt": [query_embedding, "$$this"]}, - ] - }, - ] - }, - } - }, - "magnitude1": { - "$sqrt": { - "$reduce": { - "input": "$embedding", - "initialValue": 0, - "in": {"$add": ["$$value", {"$multiply": ["$$this", "$$this"]}]}, - } - } - }, - "magnitude2": { - "$sqrt": { - "$reduce": { - "input": query_embedding, - "initialValue": 0, - "in": {"$add": ["$$value", {"$multiply": ["$$this", "$$this"]}]}, - } - } - }, - } - }, - # 防止除以零错误,添加一个小的 epsilon - {"$addFields": {"similarity": {"$divide": ["$dotProduct", {"$max": [{"$multiply": ["$magnitude1", "$magnitude2"]}, 1e-9]}]}}}, - { - "$match": { - "similarity": {"$gte": threshold} # 只保留相似度大于等于阈值的结果 - } - }, - {"$sort": {"similarity": -1}}, - {"$limit": limit}, - {"$project": {"content": 1, "similarity": 1}}, - ] - - try: - results = list(db.knowledges.aggregate(pipeline)) - # 注意:这里的 logger 需要能访问到,或者在这个函数里获取 logger 实例 - # logger.debug(f"旧知识库查询结果数量: {len(results)}") # 暂时注释掉,避免 logger 未定义 - except Exception as e: - logger.debug(f"执行旧知识库聚合查询时出错: {e}") - results = [] - - if not results: - return "" if not return_raw else [] - - if return_raw: - return results - else: - # 返回所有找到的内容,用换行分隔 - return "\n".join(str(result["content"]) for result in results) - + return "wait", f"行动规划处理中发生错误,暂时等待: {str(e)}" \ No newline at end of file diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index 3599e6e1..cf0af43f 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -6,6 +6,10 @@ from src.plugins.knowledge.knowledge_lib import qa_manager from src.common.database import db # 用于获取文本的嵌入向量 (旧知识库需要) from src.plugins.chat.utils import get_embedding +# --- NEW IMPORT --- +# 从 heartflow 导入知识检索和数据库查询函数/实例 +from src.plugins.heartFC_chat.heartflow_prompt_builder import prompt_builder, get_info_from_db +# --- END NEW IMPORT --- # 可能用于旧知识库提取主题 (如果需要回退到旧方法) # import jieba # 如果报错说找不到 jieba,可能需要安装: pip install jieba # import re # 正则表达式库,通常 Python 自带 @@ -55,7 +59,7 @@ PROMPT_DIRECT_REPLY = """{persona_text}。现在你在参与一场QQ私聊,请 请直接输出回复内容,不需要任何额外格式。""" # Prompt for send_new_message (追问/补充) -PROMPT_SEND_NEW_MESSAGE = """{persona_text}。现在你在参与一场QQ私聊,**刚刚你已经发送了一条或多条消息**,现在请根据以下信息再发一条新消息: +PROMPT_SEND_NEW_MESSAGE = """{persona_text}。现在你在参与一场QQ私聊,**刚刚你已经发送了一条或多条消息**,现在请根据以下信息再发一条新消息: 当前对话目标:{goals_str} @@ -68,7 +72,7 @@ PROMPT_SEND_NEW_MESSAGE = """{persona_text}。现在你在参与一场QQ私聊 {retrieved_memory_str} -请根据上述信息,结合聊天记录,继续发一条新消息(例如对之前消息的补充,深入话题,或追问等等)。该消息应该: +请根据上述信息,结合聊天记录,继续发一条新消息(例如对之前消息的补充,深入话题,或追问等等)。该消息应该: 1. 符合对话目标,以"你"的角度发言(不要自己与自己对话!) 2. 符合你的性格特征和身份细节 3. 通俗易懂,自然流畅,像正常聊天一样,简短(通常20字以内,除非特殊情况) @@ -116,6 +120,8 @@ class ReplyGenerator: self.private_name = private_name self.chat_observer = ChatObserver.get_instance(stream_id, private_name) self.reply_checker = ReplyChecker(stream_id, private_name) + + # _get_memory_info 保持不变,因为它不是与 heartflow 重复的部分 async def _get_memory_info(self, text: str) -> str: """根据文本自动检索相关记忆""" memory_prompt = "" @@ -144,104 +150,10 @@ class ReplyGenerator: # memory_prompt = "检索记忆时出错。\n" # 可以选择是否提示错误 return memory_prompt - async def _get_prompt_info_old(self, message: str, threshold: float) -> str: - """ - 旧版的知识检索方法,根据消息文本从旧知识库(knowledges collection)检索。 - (移植并简化自 heartflow_prompt_builder.py) - """ - related_info = "" - start_time = time.time() - logger.debug(f"[私聊][{self.private_name}]开始使用旧版知识检索,消息: {message[:30]}...") + # --- REMOVED _get_prompt_info_old --- - # 简化处理:直接使用整个消息进行查询,不再提取主题 - query_text = message.strip() - if not query_text: - logger.debug(f"[私聊][{self.private_name}]旧版知识检索:消息为空,跳过。") - return "" + # --- REMOVED _get_prompt_info --- - embedding = None - try: - embedding = await get_embedding(query_text, request_type="pfc_implicit_knowledge") - except Exception as e: - logger.error(f"[私聊][{self.private_name}]旧版知识检索:获取嵌入向量时出错: {str(e)}") - - if not embedding: - logger.error(f"[私聊][{self.private_name}]旧版知识检索:获取嵌入向量失败。") - return "" - - # 调用我们之前添加的 get_info_from_db 函数 - results = get_info_from_db(embedding, limit=5, threshold=threshold, return_raw=True) # 最多查 5 条 - - logger.info(f"[私聊][{self.private_name}]旧版知识库查询完成,耗时: {time.time() - start_time:.3f}秒,获取{len(results)}条结果") - - # 去重和格式化 - unique_contents = set() - final_results_content = [] - for result in results: - content = result.get("content", "").strip() - # similarity = result.get("similarity", 0.0) - if content and content not in unique_contents: - unique_contents.add(content) - # 可以选择性地加入相似度信息,或者只加内容 - # final_results_content.append(f"[{similarity:.2f}] {content}") - final_results_content.append(content) - - if final_results_content: - related_info = "\n".join(final_results_content) - logger.debug(f"[私聊][{self.private_name}]旧版知识检索格式化后内容: {related_info[:100]}...") - else: - logger.debug(f"[私聊][{self.private_name}]旧版知识检索未找到合适结果或结果为空。") - - logger.info(f"[私聊][{self.private_name}]旧版知识检索总耗时: {time.time() - start_time:.3f}秒") - return related_info - - async def _get_prompt_info(self, message: str, threshold: float = 0.38) -> str: - """ - 自动检索相关知识的主函数。优先使用 LPMM,失败则回退到旧版。 - (移植自 heartflow_prompt_builder.py) - """ - related_info = "" - start_time = time.time() - message = message.strip() - if not message: - logger.debug(f"[私聊][{self.private_name}]自动知识检索:输入消息为空。") - return "" - - logger.debug(f"[私聊][{self.private_name}]开始自动知识检索,消息: {message[:30]}...") - - # 1. 尝试从 LPMM 知识库获取知识 - try: - found_knowledge_from_lpmm = qa_manager.get_knowledge(message) - if found_knowledge_from_lpmm and found_knowledge_from_lpmm.strip(): - related_info = found_knowledge_from_lpmm.strip() - logger.info(f"[私聊][{self.private_name}]从 LPMM 知识库获取到知识,长度: {len(related_info)}") - logger.debug(f"[私聊][{self.private_name}]LPMM 知识内容: {related_info[:100]}...") - # LPMM 成功获取,直接返回 - logger.info(f"[私聊][{self.private_name}]自动知识检索(LPMM)耗时: {time.time() - start_time:.3f}秒") - return related_info - else: - logger.debug(f"[私聊][{self.private_name}]LPMM 知识库未返回有效知识,尝试旧版数据库检索。") - except Exception as e: - logger.error(f"[私聊][{self.private_name}]调用 LPMM 知识库 (qa_manager.get_knowledge) 时发生异常: {str(e)},尝试旧版数据库检索。") - - # 2. 如果 LPMM 失败或无结果,尝试旧版数据库 - try: - knowledge_from_old = await self._get_prompt_info_old(message, threshold=threshold) - if knowledge_from_old and knowledge_from_old.strip(): - related_info = knowledge_from_old.strip() - logger.info(f"[私聊][{self.private_name}]从旧版数据库检索到知识,长度: {len(related_info)}") - # 旧版成功获取,返回 - logger.info(f"[私聊][{self.private_name}]自动知识检索(旧版)耗时: {time.time() - start_time:.3f}秒") - return related_info - else: - logger.debug(f"[私聊][{self.private_name}]旧版数据库也未检索到有效知识。") - - except Exception as e2: - logger.error(f"[私聊][{self.private_name}]调用旧版知识库检索 (_get_prompt_info_old) 时也发生异常: {str(e2)}") - # 如果两种方法都失败或无结果 - logger.info(f"[私聊][{self.private_name}]自动知识检索总耗时: {time.time() - start_time:.3f}秒,未找到任何相关知识。") - return "" # 返回空字符串 - # 修改 generate 方法签名,增加 action_type 参数 async def generate( self, observation_info: ObservationInfo, conversation_info: ConversationInfo, action_type: str @@ -281,37 +193,6 @@ class ReplyGenerator: else: goals_str = "- 目前没有明确对话目标\n" # 简化无目标情况 - # --- 新增:构建知识信息字符串 --- - # knowledge_info_str = "【供参考的相关知识和记忆】\n" # 稍微改下标题,表明是供参考 - # try: - # 检查 conversation_info 是否有 knowledge_list 并且不为空 - # if hasattr(conversation_info, "knowledge_list") and conversation_info.knowledge_list: - # 最多只显示最近的 5 条知识 - # recent_knowledge = conversation_info.knowledge_list[-5:] - # for i, knowledge_item in enumerate(recent_knowledge): - # if isinstance(knowledge_item, dict): - # query = knowledge_item.get("query", "未知查询") - # knowledge = knowledge_item.get("knowledge", "无知识内容") - # source = knowledge_item.get("source", "未知来源") - # 只取知识内容的前 2000 个字 - # knowledge_snippet = knowledge[:2000] + "..." if len(knowledge) > 2000 else knowledge - # knowledge_info_str += ( - # f"{i + 1}. 关于 '{query}' (来源: {source}): {knowledge_snippet}\n" # 格式微调,更简洁 - # ) - # else: - # knowledge_info_str += f"{i + 1}. 发现一条格式不正确的知识记录。\n" - - # if not recent_knowledge: - # knowledge_info_str += "- 暂无。\n" # 更简洁的提示 - - # else: - # knowledge_info_str += "- 暂无。\n" - # except AttributeError: - # logger.warning(f"[私聊][{self.private_name}]ConversationInfo 对象可能缺少 knowledge_list 属性。") - # knowledge_info_str += "- 获取知识列表时出错。\n" - # except Exception as e: - # logger.error(f"[私聊][{self.private_name}]构建知识信息字符串时出错: {e}") - # knowledge_info_str += "- 处理知识列表时出错。\n" # 获取聊天历史记录 (chat_history_text) chat_history_text = observation_info.chat_history_str @@ -336,7 +217,7 @@ class ReplyGenerator: retrieval_context = chat_history_text if retrieval_context and retrieval_context != "还没有聊天记录。" and retrieval_context != "[构建聊天记录出错]": try: - # 提取记忆 + # 提取记忆 (调用本地的 _get_memory_info) logger.debug(f"[私聊][{self.private_name}]开始自动检索记忆...") retrieved_memory_str = await self._get_memory_info(text=retrieval_context) if retrieved_memory_str: @@ -344,9 +225,13 @@ class ReplyGenerator: else: logger.info(f"[私聊][{self.private_name}]未自动检索到相关记忆。") - # 提取知识 - logger.debug(f"[私聊][{self.private_name}]开始自动检索知识...") - retrieved_knowledge_str = await self._get_prompt_info(message=retrieval_context) + # --- MODIFIED KNOWLEDGE RETRIEVAL --- + # 提取知识 (调用导入的 prompt_builder.get_prompt_info) + logger.debug(f"[私聊][{self.private_name}]开始自动检索知识 (使用导入函数)...") + # 使用导入的 prompt_builder 实例及其方法 + retrieved_knowledge_str = await prompt_builder.get_prompt_info(message=retrieval_context, threshold=0.38) + # --- END MODIFIED KNOWLEDGE RETRIEVAL --- + if retrieved_knowledge_str: logger.info(f"[私聊][{self.private_name}]自动检索到相关知识。") else: @@ -377,7 +262,7 @@ class ReplyGenerator: persona_text=persona_text, goals_str=goals_str, chat_history_text=chat_history_text, - # knowledge_info_str=knowledge_info_str, + # knowledge_info_str=knowledge_info_str, # 移除了这个旧的知识展示方式 retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", # 如果为空则提示无 retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。" # 如果为空则提示无 ) @@ -402,82 +287,3 @@ class ReplyGenerator: (此方法逻辑保持不变) """ return await self.reply_checker.check(reply, goal, chat_history, chat_history_str, retry_count) - -def get_info_from_db( - query_embedding: list, limit: int = 1, threshold: float = 0.5, return_raw: bool = False -) -> Union[str, list]: - """ - 从旧知识库 (knowledges collection) 中根据嵌入向量相似度检索信息。 - (移植自 heartflow_prompt_builder.py) - """ - if not query_embedding: - return "" if not return_raw else [] - # 使用余弦相似度计算 - pipeline = [ - { - "$addFields": { - "dotProduct": { - "$reduce": { - "input": {"$range": [0, {"$size": "$embedding"}]}, - "initialValue": 0, - "in": { - "$add": [ - "$$value", - { - "$multiply": [ - {"$arrayElemAt": ["$embedding", "$$this"]}, - {"$arrayElemAt": [query_embedding, "$$this"]}, - ] - }, - ] - }, - } - }, - "magnitude1": { - "$sqrt": { - "$reduce": { - "input": "$embedding", - "initialValue": 0, - "in": {"$add": ["$$value", {"$multiply": ["$$this", "$$this"]}]}, - } - } - }, - "magnitude2": { - "$sqrt": { - "$reduce": { - "input": query_embedding, - "initialValue": 0, - "in": {"$add": ["$$value", {"$multiply": ["$$this", "$$this"]}]}, - } - } - }, - } - }, - # 防止除以零错误,添加一个小的 epsilon - {"$addFields": {"similarity": {"$divide": ["$dotProduct", {"$max": [{"$multiply": ["$magnitude1", "$magnitude2"]}, 1e-9]}]}}}, - { - "$match": { - "similarity": {"$gte": threshold} # 只保留相似度大于等于阈值的结果 - } - }, - {"$sort": {"similarity": -1}}, - {"$limit": limit}, - {"$project": {"content": 1, "similarity": 1}}, - ] - - try: - results = list(db.knowledges.aggregate(pipeline)) - # 注意:这里的 logger 需要能访问到,或者在这个函数里获取 logger 实例 - # logger.debug(f"旧知识库查询结果数量: {len(results)}") # 暂时注释掉,避免 logger 未定义 - except Exception as e: - logger.debug(f"执行旧知识库聚合查询时出错: {e}") - results = [] - - if not results: - return "" if not return_raw else [] - - if return_raw: - return results - else: - # 返回所有找到的内容,用换行分隔 - return "\n".join(str(result["content"]) for result in results) From a034ea61033acd945861955e095242209960bba9 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Sat, 3 May 2025 22:08:54 +0800 Subject: [PATCH 60/67] ruff --- src/plugins/PFC/action_planner.py | 5 +---- src/plugins/PFC/reply_generator.py | 11 +---------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 8d70f261..582b98c4 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -1,9 +1,6 @@ import time -from typing import Tuple, Optional, Union # 增加了 Optional +from typing import Tuple, Optional from src.plugins.memory_system.Hippocampus import HippocampusManager -from src.plugins.knowledge.knowledge_lib import qa_manager -from src.common.database import db -from src.plugins.chat.utils import get_embedding # --- NEW IMPORT --- # 从 heartflow 导入知识检索和数据库查询函数/实例 from src.plugins.heartFC_chat.heartflow_prompt_builder import prompt_builder diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index c2614b11..88987129 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -1,14 +1,6 @@ # 用于访问记忆系统 from src.plugins.memory_system.Hippocampus import HippocampusManager -# 用于访问新的知识库 (LPMM) -from src.plugins.knowledge.knowledge_lib import qa_manager - -# 用于访问数据库 (旧知识库需要) -from src.common.database import db - -# 用于获取文本的嵌入向量 (旧知识库需要) -from src.plugins.chat.utils import get_embedding # --- NEW IMPORT --- # 从 heartflow 导入知识检索和数据库查询函数/实例 from src.plugins.heartFC_chat.heartflow_prompt_builder import prompt_builder @@ -16,7 +8,7 @@ from src.plugins.heartFC_chat.heartflow_prompt_builder import prompt_builder # 可能用于旧知识库提取主题 (如果需要回退到旧方法) # import jieba # 如果报错说找不到 jieba,可能需要安装: pip install jieba # import re # 正则表达式库,通常 Python 自带 -from typing import Tuple, List, Dict, Any, Union +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 @@ -26,7 +18,6 @@ from src.individuality.individuality import Individuality from .observation_info import ObservationInfo from .conversation_info import ConversationInfo from src.plugins.utils.chat_message_builder import build_readable_messages -import time logger = get_module_logger("reply_generator") From dff8fef12954710f8437497fe182961aab67b632 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 3 May 2025 14:09:18 +0000 Subject: [PATCH 61/67] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/action_planner.py | 13 +++++++++---- src/plugins/PFC/reply_generator.py | 12 ++++++++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 582b98c4..51d9bff2 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -1,9 +1,11 @@ import time from typing import Tuple, Optional from src.plugins.memory_system.Hippocampus import HippocampusManager + # --- NEW IMPORT --- # 从 heartflow 导入知识检索和数据库查询函数/实例 from src.plugins.heartFC_chat.heartflow_prompt_builder import prompt_builder + # --- END NEW IMPORT --- # import jieba # 如果需要旧版知识库的回退,可能需要 # import re # 如果需要旧版知识库的回退,可能需要 @@ -268,7 +270,6 @@ class ActionPlanner: logger.error(f"[私聊][{self.private_name}]构建对话目标字符串时出错: {e}") goals_str = "- 构建对话目标时出错。\n" - # 获取聊天历史记录 (chat_history_text) try: if hasattr(observation_info, "chat_history") and observation_info.chat_history: @@ -392,9 +393,13 @@ class ActionPlanner: # 调用导入的 prompt_builder.get_prompt_info logger.debug(f"[私聊][{self.private_name}] (ActionPlanner) 开始自动检索知识 (使用导入函数)...") # 使用导入的 prompt_builder 实例及其方法 - retrieved_knowledge_str_planner = await prompt_builder.get_prompt_info(message=retrieval_context, threshold=0.38) + retrieved_knowledge_str_planner = await prompt_builder.get_prompt_info( + message=retrieval_context, threshold=0.38 + ) # --- END MODIFIED KNOWLEDGE RETRIEVAL --- - logger.info(f"[私聊][{self.private_name}] (ActionPlanner) 自动检索知识 {'完成' if retrieved_knowledge_str_planner else '无结果'}。") + logger.info( + f"[私聊][{self.private_name}] (ActionPlanner) 自动检索知识 {'完成' if retrieved_knowledge_str_planner else '无结果'}。" + ) except Exception as retrieval_err: logger.error(f"[私聊][{self.private_name}] (ActionPlanner) 自动检索时出错: {retrieval_err}") @@ -528,4 +533,4 @@ class ActionPlanner: except Exception as e: # 外层异常处理保持不变 logger.error(f"[私聊][{self.private_name}]规划行动时调用 LLM 或处理结果出错: {str(e)}") - return "wait", f"行动规划处理中发生错误,暂时等待: {str(e)}" \ No newline at end of file + return "wait", f"行动规划处理中发生错误,暂时等待: {str(e)}" diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index 88987129..b9d2c00e 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -4,6 +4,7 @@ from src.plugins.memory_system.Hippocampus import HippocampusManager # --- NEW IMPORT --- # 从 heartflow 导入知识检索和数据库查询函数/实例 from src.plugins.heartFC_chat.heartflow_prompt_builder import prompt_builder + # --- END NEW IMPORT --- # 可能用于旧知识库提取主题 (如果需要回退到旧方法) # import jieba # 如果报错说找不到 jieba,可能需要安装: pip install jieba @@ -187,7 +188,6 @@ class ReplyGenerator: else: goals_str = "- 目前没有明确对话目标\n" # 简化无目标情况 - # 获取聊天历史记录 (chat_history_text) chat_history_text = observation_info.chat_history_str if observation_info.new_messages_count > 0 and observation_info.unprocessed_messages: @@ -223,7 +223,9 @@ class ReplyGenerator: # 提取知识 (调用导入的 prompt_builder.get_prompt_info) logger.debug(f"[私聊][{self.private_name}]开始自动检索知识 (使用导入函数)...") # 使用导入的 prompt_builder 实例及其方法 - retrieved_knowledge_str = await prompt_builder.get_prompt_info(message=retrieval_context, threshold=0.38) + retrieved_knowledge_str = await prompt_builder.get_prompt_info( + message=retrieval_context, threshold=0.38 + ) # --- END MODIFIED KNOWLEDGE RETRIEVAL --- if retrieved_knowledge_str: @@ -257,8 +259,10 @@ class ReplyGenerator: goals_str=goals_str, chat_history_text=chat_history_text, # knowledge_info_str=knowledge_info_str, # 移除了这个旧的知识展示方式 - retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", # 如果为空则提示无 - retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。" # 如果为空则提示无 + retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", # 如果为空则提示无 + retrieved_knowledge_str=retrieved_knowledge_str + if retrieved_knowledge_str + else "无相关知识。", # 如果为空则提示无 ) # --- 调用 LLM 生成 --- From 7737a95e405c21c34cfd23cd78689e60a3ce6d22 Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Sun, 4 May 2025 03:43:59 +0800 Subject: [PATCH 62/67] =?UTF-8?q?=E5=86=8D=E6=AC=A1=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=8F=AF=E8=83=BD=E7=9A=84=E5=A4=8D=E8=AF=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/conversation.py | 22 +++++++++++-- src/plugins/PFC/conversation_info.py | 2 ++ src/plugins/PFC/reply_generator.py | 49 ++++++++++++++++++++++------ 3 files changed, 61 insertions(+), 12 deletions(-) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 9a0dd36b..5e04e992 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -337,6 +337,14 @@ class Conversation: logger.info( f"[私聊][{self.private_name}]第 {reply_attempt_count} 次追问检查结果: 合适={is_suitable}, 原因='{check_reason}', 需重新规划={need_replan}" ) + if not is_suitable or need_replan: + conversation_info.last_reply_rejection_reason = check_reason + conversation_info.last_rejected_reply_content = self.generated_reply + else: + # 如果检查通过,清空上次的拒绝原因 + conversation_info.last_reply_rejection_reason = None + conversation_info.last_rejected_reply_content = None + if is_suitable: final_reply_to_send = self.generated_reply break @@ -350,8 +358,11 @@ class Conversation: f"[私聊][{self.private_name}]第 {reply_attempt_count} 次调用 ReplyChecker (追问) 时出错: {check_err}" ) check_reason = f"第 {reply_attempt_count} 次检查过程出错: {check_err}" + conversation_info.last_reply_rejection_reason = f"检查过程出错: {check_err}" # 出错也记录原因 + conversation_info.last_rejected_reply_content = self.generated_reply break + # 循环结束,处理最终结果 if is_suitable: # 检查是否有新消息 @@ -366,7 +377,9 @@ class Conversation: self.generated_reply = final_reply_to_send # --- 在这里调用 _send_reply --- await self._send_reply() # <--- 调用恢复后的函数 - + # --- 新增:回复成功,清除拒绝原因 --- + conversation_info.last_reply_rejection_reason = None + conversation_info.last_rejected_reply_content = None # 更新状态: 标记上次成功是 send_new_message self.conversation_info.last_successful_reply_action = "send_new_message" action_successful = True # 标记动作成功 @@ -470,7 +483,9 @@ class Conversation: self.generated_reply = final_reply_to_send # --- 在这里调用 _send_reply --- await self._send_reply() # <--- 调用恢复后的函数 - + # --- 新增:回复成功,清除拒绝原因 --- + conversation_info.last_reply_rejection_reason = None + conversation_info.last_rejected_reply_content = None # <-- 新增清空内容 # 更新状态: 标记上次成功是 direct_reply self.conversation_info.last_successful_reply_action = "direct_reply" action_successful = True # 标记动作成功 @@ -649,6 +664,9 @@ class Conversation: # 重置状态: 对于非回复类动作的成功,清除上次回复状态 if action not in ["direct_reply", "send_new_message"]: self.conversation_info.last_successful_reply_action = None + # --- 新增:非回复动作成功,也清除拒绝原因 --- + conversation_info.last_reply_rejection_reason = None + conversation_info.last_rejected_reply_content = None # <-- 新增清空内容 logger.debug(f"[私聊][{self.private_name}]动作 {action} 成功完成,重置 last_successful_reply_action") # 如果动作是 recall 状态,在各自的处理逻辑中已经更新了 done_action diff --git a/src/plugins/PFC/conversation_info.py b/src/plugins/PFC/conversation_info.py index 04524b69..be754b3c 100644 --- a/src/plugins/PFC/conversation_info.py +++ b/src/plugins/PFC/conversation_info.py @@ -8,3 +8,5 @@ class ConversationInfo: self.knowledge_list = [] self.memory_list = [] self.last_successful_reply_action: Optional[str] = None + self.last_reply_rejection_reason: Optional[str] = None # 用于存储上次回复被拒原因 + self.last_rejected_reply_content: Optional[str] = None # 用于存储上次被拒的回复内容 \ No newline at end of file diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index b9d2c00e..c98b8d1c 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -38,6 +38,8 @@ PROMPT_DIRECT_REPLY = """{persona_text}。现在你在参与一场QQ私聊,请 {retrieved_memory_str} +{last_rejection_info} + 请根据上述信息,结合聊天记录,回复对方。该回复应该: 1. 符合对话目标,以"你"的角度发言(不要自己与自己对话!) @@ -67,6 +69,8 @@ PROMPT_SEND_NEW_MESSAGE = """{persona_text}。现在你在参与一场QQ私聊 {retrieved_memory_str} +{last_rejection_info} + 请根据上述信息,结合聊天记录,继续发一条新消息(例如对之前消息的补充,深入话题,或追问等等)。该消息应该: 1. 符合对话目标,以"你"的角度发言(不要自己与自己对话!) 2. 符合你的性格特征和身份细节 @@ -242,6 +246,25 @@ class ReplyGenerator: retrieved_memory_str = "无聊天记录,无法自动检索记忆。\n" retrieved_knowledge_str = "无聊天记录,无法自动检索知识。\n" + # --- 修改:构建上次回复失败原因和内容提示 --- + last_rejection_info_str = "" + # 检查 conversation_info 是否有上次拒绝的原因和内容,并且它们都不是 None + last_reason = getattr(conversation_info, 'last_reply_rejection_reason', None) + last_content = getattr(conversation_info, 'last_rejected_reply_content', None) + + if last_reason and last_content: + last_rejection_info_str = ( + f"\n------\n" + f"【重要提示:你上一次尝试回复时失败了,以下是详细信息】\n" + f"上次试图发送的消息内容: “{last_content}”\n" # <-- 显示上次内容 + f"失败原因: “{last_reason}”\n" + f"请根据【消息内容】和【失败原因】调整你的新回复,避免重复之前的错误。\n" + f"------\n" + ) + logger.info(f"[私聊][{self.private_name}]检测到上次回复失败信息,将加入 Prompt:\n" + f" 内容: {last_content}\n" + f" 原因: {last_reason}") + # --- 选择 Prompt --- if action_type == "send_new_message": prompt_template = PROMPT_SEND_NEW_MESSAGE @@ -254,16 +277,22 @@ class ReplyGenerator: logger.info(f"[私聊][{self.private_name}]使用 PROMPT_DIRECT_REPLY (首次/非连续回复生成)") # --- 格式化最终的 Prompt --- - prompt = prompt_template.format( - persona_text=persona_text, - goals_str=goals_str, - chat_history_text=chat_history_text, - # knowledge_info_str=knowledge_info_str, # 移除了这个旧的知识展示方式 - retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", # 如果为空则提示无 - retrieved_knowledge_str=retrieved_knowledge_str - if retrieved_knowledge_str - else "无相关知识。", # 如果为空则提示无 - ) + try: # <--- 增加 try-except 块处理可能的 format 错误 + prompt = prompt_template.format( + persona_text=persona_text, + goals_str=goals_str, + chat_history_text=chat_history_text, + retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", + retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。", + last_rejection_info=last_rejection_info_str # <--- 新增传递上次拒绝原因 + ) + except KeyError as e: + logger.error(f"[私聊][{self.private_name}]格式化 Prompt 时出错,缺少键: {e}。请检查 Prompt 模板和传递的参数。") + # 返回错误信息或默认回复 + return "抱歉,准备回复时出了点问题,请检查一下我的代码..." + except Exception as fmt_err: + logger.error(f"[私聊][{self.private_name}]格式化 Prompt 时发生未知错误: {fmt_err}") + return "抱歉,准备回复时出了点内部错误,请检查一下我的代码..." # --- 调用 LLM 生成 --- logger.debug(f"[私聊][{self.private_name}]发送到LLM的生成提示词:\n------\n{prompt}\n------") From 741fa815c040206011bdc4381fae1cbe93d920e2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 3 May 2025 19:44:13 +0000 Subject: [PATCH 63/67] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/conversation.py | 9 ++++----- src/plugins/PFC/conversation_info.py | 4 ++-- src/plugins/PFC/reply_generator.py | 30 ++++++++++++++++------------ 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 5e04e992..66dbbf7b 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -341,7 +341,7 @@ class Conversation: conversation_info.last_reply_rejection_reason = check_reason conversation_info.last_rejected_reply_content = self.generated_reply else: - # 如果检查通过,清空上次的拒绝原因 + # 如果检查通过,清空上次的拒绝原因 conversation_info.last_reply_rejection_reason = None conversation_info.last_rejected_reply_content = None @@ -358,11 +358,10 @@ class Conversation: f"[私聊][{self.private_name}]第 {reply_attempt_count} 次调用 ReplyChecker (追问) 时出错: {check_err}" ) check_reason = f"第 {reply_attempt_count} 次检查过程出错: {check_err}" - conversation_info.last_reply_rejection_reason = f"检查过程出错: {check_err}" # 出错也记录原因 + conversation_info.last_reply_rejection_reason = f"检查过程出错: {check_err}" # 出错也记录原因 conversation_info.last_rejected_reply_content = self.generated_reply break - # 循环结束,处理最终结果 if is_suitable: # 检查是否有新消息 @@ -485,7 +484,7 @@ class Conversation: await self._send_reply() # <--- 调用恢复后的函数 # --- 新增:回复成功,清除拒绝原因 --- conversation_info.last_reply_rejection_reason = None - conversation_info.last_rejected_reply_content = None # <-- 新增清空内容 + conversation_info.last_rejected_reply_content = None # <-- 新增清空内容 # 更新状态: 标记上次成功是 direct_reply self.conversation_info.last_successful_reply_action = "direct_reply" action_successful = True # 标记动作成功 @@ -666,7 +665,7 @@ class Conversation: self.conversation_info.last_successful_reply_action = None # --- 新增:非回复动作成功,也清除拒绝原因 --- conversation_info.last_reply_rejection_reason = None - conversation_info.last_rejected_reply_content = None # <-- 新增清空内容 + conversation_info.last_rejected_reply_content = None # <-- 新增清空内容 logger.debug(f"[私聊][{self.private_name}]动作 {action} 成功完成,重置 last_successful_reply_action") # 如果动作是 recall 状态,在各自的处理逻辑中已经更新了 done_action diff --git a/src/plugins/PFC/conversation_info.py b/src/plugins/PFC/conversation_info.py index be754b3c..062a4641 100644 --- a/src/plugins/PFC/conversation_info.py +++ b/src/plugins/PFC/conversation_info.py @@ -8,5 +8,5 @@ class ConversationInfo: self.knowledge_list = [] self.memory_list = [] self.last_successful_reply_action: Optional[str] = None - self.last_reply_rejection_reason: Optional[str] = None # 用于存储上次回复被拒原因 - self.last_rejected_reply_content: Optional[str] = None # 用于存储上次被拒的回复内容 \ No newline at end of file + self.last_reply_rejection_reason: Optional[str] = None # 用于存储上次回复被拒原因 + self.last_rejected_reply_content: Optional[str] = None # 用于存储上次被拒的回复内容 diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index c98b8d1c..5c4eb46c 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -249,21 +249,23 @@ class ReplyGenerator: # --- 修改:构建上次回复失败原因和内容提示 --- last_rejection_info_str = "" # 检查 conversation_info 是否有上次拒绝的原因和内容,并且它们都不是 None - last_reason = getattr(conversation_info, 'last_reply_rejection_reason', None) - last_content = getattr(conversation_info, 'last_rejected_reply_content', None) + last_reason = getattr(conversation_info, "last_reply_rejection_reason", None) + last_content = getattr(conversation_info, "last_rejected_reply_content", None) if last_reason and last_content: last_rejection_info_str = ( f"\n------\n" f"【重要提示:你上一次尝试回复时失败了,以下是详细信息】\n" - f"上次试图发送的消息内容: “{last_content}”\n" # <-- 显示上次内容 + f"上次试图发送的消息内容: “{last_content}”\n" # <-- 显示上次内容 f"失败原因: “{last_reason}”\n" f"请根据【消息内容】和【失败原因】调整你的新回复,避免重复之前的错误。\n" f"------\n" ) - logger.info(f"[私聊][{self.private_name}]检测到上次回复失败信息,将加入 Prompt:\n" - f" 内容: {last_content}\n" - f" 原因: {last_reason}") + logger.info( + f"[私聊][{self.private_name}]检测到上次回复失败信息,将加入 Prompt:\n" + f" 内容: {last_content}\n" + f" 原因: {last_reason}" + ) # --- 选择 Prompt --- if action_type == "send_new_message": @@ -277,22 +279,24 @@ class ReplyGenerator: logger.info(f"[私聊][{self.private_name}]使用 PROMPT_DIRECT_REPLY (首次/非连续回复生成)") # --- 格式化最终的 Prompt --- - try: # <--- 增加 try-except 块处理可能的 format 错误 + try: # <--- 增加 try-except 块处理可能的 format 错误 prompt = prompt_template.format( persona_text=persona_text, goals_str=goals_str, chat_history_text=chat_history_text, retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。", retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。", - last_rejection_info=last_rejection_info_str # <--- 新增传递上次拒绝原因 + last_rejection_info=last_rejection_info_str, # <--- 新增传递上次拒绝原因 ) except KeyError as e: - logger.error(f"[私聊][{self.private_name}]格式化 Prompt 时出错,缺少键: {e}。请检查 Prompt 模板和传递的参数。") - # 返回错误信息或默认回复 - return "抱歉,准备回复时出了点问题,请检查一下我的代码..." + logger.error( + f"[私聊][{self.private_name}]格式化 Prompt 时出错,缺少键: {e}。请检查 Prompt 模板和传递的参数。" + ) + # 返回错误信息或默认回复 + return "抱歉,准备回复时出了点问题,请检查一下我的代码..." except Exception as fmt_err: - logger.error(f"[私聊][{self.private_name}]格式化 Prompt 时发生未知错误: {fmt_err}") - return "抱歉,准备回复时出了点内部错误,请检查一下我的代码..." + logger.error(f"[私聊][{self.private_name}]格式化 Prompt 时发生未知错误: {fmt_err}") + return "抱歉,准备回复时出了点内部错误,请检查一下我的代码..." # --- 调用 LLM 生成 --- logger.debug(f"[私聊][{self.private_name}]发送到LLM的生成提示词:\n------\n{prompt}\n------") From acbabdc24b937b920faadc1ce614048350f3c792 Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Sun, 4 May 2025 17:45:13 +0800 Subject: [PATCH 64/67] =?UTF-8?q?=E7=A9=B6=E6=9E=81=E7=9A=84=E5=8F=AF?= =?UTF-8?q?=E7=BB=B4=E6=8A=A4=E6=80=A7=E5=92=8C=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/action_planner.py | 79 ++------------------------ src/plugins/PFC/pfc_utils.py | 72 +++++++++++++++++++++++- src/plugins/PFC/reply_generator.py | 90 ++++-------------------------- 3 files changed, 85 insertions(+), 156 deletions(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 51d9bff2..5cba1bb1 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -1,12 +1,6 @@ import time from typing import Tuple, Optional -from src.plugins.memory_system.Hippocampus import HippocampusManager - -# --- NEW IMPORT --- -# 从 heartflow 导入知识检索和数据库查询函数/实例 -from src.plugins.heartFC_chat.heartflow_prompt_builder import prompt_builder - -# --- END NEW IMPORT --- +from .pfc_utils import retrieve_contextual_info # import jieba # 如果需要旧版知识库的回退,可能需要 # import re # 如果需要旧版知识库的回退,可能需要 from src.common.logger_manager import get_logger @@ -128,41 +122,6 @@ class ActionPlanner: self.private_name = private_name self.chat_observer = ChatObserver.get_instance(stream_id, private_name) - # _get_memory_info 保持不变 - async def _get_memory_info(self, text: str) -> str: - """根据文本自动检索相关记忆""" - memory_prompt = "" - related_memory_info = "" - try: - related_memory = await HippocampusManager.get_instance().get_memory_from_text( - text=text, - max_memory_num=2, # 最多获取 2 条记忆 - max_memory_length=2, # 每条记忆长度限制(这个参数含义可能需确认) - max_depth=3, # 搜索深度 - fast_retrieval=False, # 是否快速检索 - ) - if related_memory: - for memory in related_memory: - # memory[0] 是记忆ID, memory[1] 是记忆内容 - related_memory_info += memory[1] + "\n" # 将记忆内容拼接起来 - if related_memory_info: - memory_prompt = f"你回忆起:\n{related_memory_info.strip()}\n(以上是你的回忆,供参考)\n" - logger.debug( - f"[私聊]决策层[{self.private_name}]自动检索到记忆: {related_memory_info.strip()[:100]}..." - ) - else: - logger.debug(f"[私聊]决策层[{self.private_name}]自动检索记忆返回为空。") - else: - logger.debug(f"[私聊]决策层[{self.private_name}]未自动检索到相关记忆。") - except Exception as e: - logger.error(f"[私聊]决策层[{self.private_name}]自动检索记忆时出错: {e}") - # memory_prompt = "检索记忆时出错。\n" # 可以选择是否提示错误 - return memory_prompt - - # --- REMOVED _get_prompt_info_old --- - - # --- REMOVED _get_prompt_info --- - # 修改 plan 方法签名,增加 last_successful_reply_action 参数 async def plan( self, @@ -377,38 +336,10 @@ class ActionPlanner: last_action_context += f"- 该行动当前状态: {status}\n" # self.last_successful_action_type = None # 非完成状态,清除记录 - retrieved_memory_str_planner = "" - retrieved_knowledge_str_planner = "" - retrieval_context = chat_history_text # 使用聊天记录作为检索上下文 - if retrieval_context and retrieval_context != "还没有聊天记录。" and retrieval_context != "[构建聊天记录出错]": - try: - # 调用本地的 _get_memory_info - logger.debug(f"[私聊][{self.private_name}] (ActionPlanner) 开始自动检索记忆...") - retrieved_memory_str_planner = await self._get_memory_info(text=retrieval_context) - logger.info( - f"[私聊][{self.private_name}] (ActionPlanner) 自动检索记忆 {'完成' if retrieved_memory_str_planner else '无结果'}。" - ) - - # --- MODIFIED KNOWLEDGE RETRIEVAL --- - # 调用导入的 prompt_builder.get_prompt_info - logger.debug(f"[私聊][{self.private_name}] (ActionPlanner) 开始自动检索知识 (使用导入函数)...") - # 使用导入的 prompt_builder 实例及其方法 - retrieved_knowledge_str_planner = await prompt_builder.get_prompt_info( - message=retrieval_context, threshold=0.38 - ) - # --- END MODIFIED KNOWLEDGE RETRIEVAL --- - logger.info( - f"[私聊][{self.private_name}] (ActionPlanner) 自动检索知识 {'完成' if retrieved_knowledge_str_planner else '无结果'}。" - ) - - except Exception as retrieval_err: - logger.error(f"[私聊][{self.private_name}] (ActionPlanner) 自动检索时出错: {retrieval_err}") - retrieved_memory_str_planner = "检索记忆时出错。\n" - retrieved_knowledge_str_planner = "检索知识时出错。\n" - else: - logger.debug(f"[私聊][{self.private_name}] (ActionPlanner) 无有效聊天记录,跳过自动检索。") - retrieved_memory_str_planner = "无聊天记录无法检索记忆。\n" - retrieved_knowledge_str_planner = "无聊天记录无法检索知识。\n" + retrieved_memory_str_planner, retrieved_knowledge_str_planner = await retrieve_contextual_info(chat_history_text, self.private_name) + # Optional: 可以加一行日志确认结果,方便调试 + logger.info(f"[私聊][{self.private_name}] (ActionPlanner) 统一检索完成。记忆: {'有' if '回忆起' in retrieved_memory_str_planner else '无'} / 知识: {'有' if '出错' not in retrieved_knowledge_str_planner and '无相关知识' not in retrieved_knowledge_str_planner else '无'}") + # --- 选择 Prompt --- if last_successful_reply_action in ["direct_reply", "send_new_message"]: diff --git a/src/plugins/PFC/pfc_utils.py b/src/plugins/PFC/pfc_utils.py index 2f7bd5e0..a7a412c1 100644 --- a/src/plugins/PFC/pfc_utils.py +++ b/src/plugins/PFC/pfc_utils.py @@ -1,9 +1,77 @@ +import traceback import json import re from typing import Dict, Any, Optional, Tuple, List, Union -from src.common.logger import get_module_logger +from src.common.logger_manager import get_logger # 确认 logger 的导入路径 +from src.plugins.memory_system.Hippocampus import HippocampusManager +from src.plugins.heartFC_chat.heartflow_prompt_builder import prompt_builder # 确认 prompt_builder 的导入路径 + +logger = get_logger("pfc_utils") + + +async def retrieve_contextual_info(text: str, private_name: str) -> Tuple[str, str]: + """ + 根据输入文本检索相关的记忆和知识。 + + Args: + text: 用于检索的上下文文本 (例如聊天记录)。 + private_name: 私聊对象的名称,用于日志记录。 + + Returns: + Tuple[str, str]: (检索到的记忆字符串, 检索到的知识字符串) + """ + retrieved_memory_str = "无相关记忆。" + retrieved_knowledge_str = "无相关知识。" + memory_log_msg = "未自动检索到相关记忆。" + knowledge_log_msg = "未自动检索到相关知识。" + + if not text or text == "还没有聊天记录。" or text == "[构建聊天记录出错]": + logger.debug(f"[私聊][{private_name}] (retrieve_contextual_info) 无有效上下文,跳过检索。") + return retrieved_memory_str, retrieved_knowledge_str + + # 1. 检索记忆 (逻辑来自原 _get_memory_info) + try: + related_memory = await HippocampusManager.get_instance().get_memory_from_text( + text=text, + max_memory_num=2, + max_memory_length=2, + max_depth=3, + fast_retrieval=False, + ) + if related_memory: + related_memory_info = "" + for memory in related_memory: + related_memory_info += memory[1] + "\n" + if related_memory_info: + # 注意:原版提示信息可以根据需要调整 + retrieved_memory_str = f"你回忆起:\n{related_memory_info.strip()}\n(以上是你的回忆,供参考)\n" + memory_log_msg = f"自动检索到记忆: {related_memory_info.strip()[:100]}..." + else: + memory_log_msg = "自动检索记忆返回为空。" + logger.debug(f"[私聊][{private_name}] (retrieve_contextual_info) 记忆检索: {memory_log_msg}") + + except Exception as e: + logger.error(f"[私聊][{private_name}] (retrieve_contextual_info) 自动检索记忆时出错: {e}\n{traceback.format_exc()}") + retrieved_memory_str = "检索记忆时出错。\n" + + # 2. 检索知识 (逻辑来自原 action_planner 和 reply_generator) + try: + # 使用导入的 prompt_builder 实例及其方法 + knowledge_result = await prompt_builder.get_prompt_info( + message=text, threshold=0.38 # threshold 可以根据需要调整 + ) + if knowledge_result: + retrieved_knowledge_str = knowledge_result # 直接使用返回结果 + knowledge_log_msg = "自动检索到相关知识。" + logger.debug(f"[私聊][{private_name}] (retrieve_contextual_info) 知识检索: {knowledge_log_msg}") + + except Exception as e: + logger.error(f"[私聊][{private_name}] (retrieve_contextual_info) 自动检索知识时出错: {e}\n{traceback.format_exc()}") + retrieved_knowledge_str = "检索知识时出错。\n" + + return retrieved_memory_str, retrieved_knowledge_str + -logger = get_module_logger("pfc_utils") def get_items_from_json( diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index c98b8d1c..33a4bb41 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -1,16 +1,10 @@ -# 用于访问记忆系统 -from src.plugins.memory_system.Hippocampus import HippocampusManager - -# --- NEW IMPORT --- -# 从 heartflow 导入知识检索和数据库查询函数/实例 -from src.plugins.heartFC_chat.heartflow_prompt_builder import prompt_builder - -# --- END NEW IMPORT --- +from .pfc_utils import retrieve_contextual_info # 可能用于旧知识库提取主题 (如果需要回退到旧方法) # import jieba # 如果报错说找不到 jieba,可能需要安装: pip install jieba # import re # 正则表达式库,通常 Python 自带 from typing import Tuple, List, Dict, Any -from src.common.logger import get_module_logger +# from src.common.logger import get_module_logger +from src.common.logger_manager import get_logger from ..models.utils_model import LLMRequest from ...config.config import global_config from .chat_observer import ChatObserver @@ -20,7 +14,7 @@ from .observation_info import ObservationInfo from .conversation_info import ConversationInfo from src.plugins.utils.chat_message_builder import build_readable_messages -logger = get_module_logger("reply_generator") +logger = get_logger("reply_generator") # --- 定义 Prompt 模板 --- @@ -120,39 +114,7 @@ class ReplyGenerator: self.chat_observer = ChatObserver.get_instance(stream_id, private_name) self.reply_checker = ReplyChecker(stream_id, private_name) - # _get_memory_info 保持不变,因为它不是与 heartflow 重复的部分 - async def _get_memory_info(self, text: str) -> str: - """根据文本自动检索相关记忆""" - memory_prompt = "" - related_memory_info = "" - try: - related_memory = await HippocampusManager.get_instance().get_memory_from_text( - text=text, - max_memory_num=2, # 最多获取 2 条记忆 - max_memory_length=2, # 每条记忆长度限制(这个参数含义可能需确认) - max_depth=3, # 搜索深度 - fast_retrieval=False, # 是否快速检索 - ) - if related_memory: - for memory in related_memory: - # memory[0] 是记忆ID, memory[1] 是记忆内容 - related_memory_info += memory[1] + "\n" # 将记忆内容拼接起来 - if related_memory_info: - memory_prompt = f"你回忆起:\n{related_memory_info.strip()}\n(以上是你的回忆,不一定是目前聊天里的人说的,回忆中别人说的事情也不一定是准确的,请记住)\n" - logger.debug(f"[私聊][{self.private_name}]自动检索到记忆: {related_memory_info.strip()[:100]}...") - else: - logger.debug(f"[私聊][{self.private_name}]自动检索记忆返回为空。") - else: - logger.debug(f"[私聊][{self.private_name}]未自动检索到相关记忆。") - except Exception as e: - logger.error(f"[私聊][{self.private_name}]自动检索记忆时出错: {e}") - # memory_prompt = "检索记忆时出错。\n" # 可以选择是否提示错误 - return memory_prompt - - # --- REMOVED _get_prompt_info_old --- - - # --- REMOVED _get_prompt_info --- - + # 修改 generate 方法签名,增加 action_type 参数 async def generate( self, observation_info: ObservationInfo, conversation_info: ConversationInfo, action_type: str @@ -209,43 +171,11 @@ class ReplyGenerator: # 构建 Persona 文本 (persona_text) persona_text = f"你的名字是{self.name},{self.personality_info}。" - retrieved_memory_str = "" - retrieved_knowledge_str = "" - # 使用 chat_history_text 作为检索的上下文,因为它包含了最近的对话和新消息 - retrieval_context = chat_history_text - if retrieval_context and retrieval_context != "还没有聊天记录。" and retrieval_context != "[构建聊天记录出错]": - try: - # 提取记忆 (调用本地的 _get_memory_info) - logger.debug(f"[私聊][{self.private_name}]开始自动检索记忆...") - retrieved_memory_str = await self._get_memory_info(text=retrieval_context) - if retrieved_memory_str: - logger.info(f"[私聊][{self.private_name}]自动检索到记忆片段。") - else: - logger.info(f"[私聊][{self.private_name}]未自动检索到相关记忆。") - - # --- MODIFIED KNOWLEDGE RETRIEVAL --- - # 提取知识 (调用导入的 prompt_builder.get_prompt_info) - logger.debug(f"[私聊][{self.private_name}]开始自动检索知识 (使用导入函数)...") - # 使用导入的 prompt_builder 实例及其方法 - retrieved_knowledge_str = await prompt_builder.get_prompt_info( - message=retrieval_context, threshold=0.38 - ) - # --- END MODIFIED KNOWLEDGE RETRIEVAL --- - - if retrieved_knowledge_str: - logger.info(f"[私聊][{self.private_name}]自动检索到相关知识。") - else: - logger.info(f"[私聊][{self.private_name}]未自动检索到相关知识。") - - except Exception as retrieval_err: - logger.error(f"[私聊][{self.private_name}]在自动检索记忆/知识时发生错误: {retrieval_err}") - retrieved_memory_str = "检索记忆时出错。\n" - retrieved_knowledge_str = "检索知识时出错。\n" - else: - logger.debug(f"[私聊][{self.private_name}]聊天记录为空或无效,跳过自动记忆/知识检索。") - retrieved_memory_str = "无聊天记录,无法自动检索记忆。\n" - retrieved_knowledge_str = "无聊天记录,无法自动检索知识。\n" - + retrieval_context = chat_history_text # 使用前面构建好的 chat_history_text + # 调用共享函数进行检索 + retrieved_memory_str, retrieved_knowledge_str = await retrieve_contextual_info(retrieval_context, self.private_name) + logger.info(f"[私聊][{self.private_name}] (ReplyGenerator) 统一检索完成。记忆: {'有' if '回忆起' in retrieved_memory_str else '无'} / 知识: {'有' if '出错' not in retrieved_knowledge_str and '无相关知识' not in retrieved_knowledge_str else '无'}") + # --- 修改:构建上次回复失败原因和内容提示 --- last_rejection_info_str = "" # 检查 conversation_info 是否有上次拒绝的原因和内容,并且它们都不是 None From bf939cdd39b8be2d7efd37d301f8925f897b48f5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 4 May 2025 09:45:46 +0000 Subject: [PATCH 65/67] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/action_planner.py | 10 +++++++--- src/plugins/PFC/pfc_utils.py | 23 +++++++++++++---------- src/plugins/PFC/reply_generator.py | 15 ++++++++++----- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 5cba1bb1..faf13bd0 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -1,6 +1,7 @@ import time from typing import Tuple, Optional from .pfc_utils import retrieve_contextual_info + # import jieba # 如果需要旧版知识库的回退,可能需要 # import re # 如果需要旧版知识库的回退,可能需要 from src.common.logger_manager import get_logger @@ -336,10 +337,13 @@ class ActionPlanner: last_action_context += f"- 该行动当前状态: {status}\n" # self.last_successful_action_type = None # 非完成状态,清除记录 - retrieved_memory_str_planner, retrieved_knowledge_str_planner = await retrieve_contextual_info(chat_history_text, self.private_name) + retrieved_memory_str_planner, retrieved_knowledge_str_planner = await retrieve_contextual_info( + chat_history_text, self.private_name + ) # Optional: 可以加一行日志确认结果,方便调试 - logger.info(f"[私聊][{self.private_name}] (ActionPlanner) 统一检索完成。记忆: {'有' if '回忆起' in retrieved_memory_str_planner else '无'} / 知识: {'有' if '出错' not in retrieved_knowledge_str_planner and '无相关知识' not in retrieved_knowledge_str_planner else '无'}") - + logger.info( + f"[私聊][{self.private_name}] (ActionPlanner) 统一检索完成。记忆: {'有' if '回忆起' in retrieved_memory_str_planner else '无'} / 知识: {'有' if '出错' not in retrieved_knowledge_str_planner and '无相关知识' not in retrieved_knowledge_str_planner else '无'}" + ) # --- 选择 Prompt --- if last_successful_reply_action in ["direct_reply", "send_new_message"]: diff --git a/src/plugins/PFC/pfc_utils.py b/src/plugins/PFC/pfc_utils.py index a7a412c1..b0f3f841 100644 --- a/src/plugins/PFC/pfc_utils.py +++ b/src/plugins/PFC/pfc_utils.py @@ -2,9 +2,9 @@ import traceback import json import re from typing import Dict, Any, Optional, Tuple, List, Union -from src.common.logger_manager import get_logger # 确认 logger 的导入路径 +from src.common.logger_manager import get_logger # 确认 logger 的导入路径 from src.plugins.memory_system.Hippocampus import HippocampusManager -from src.plugins.heartFC_chat.heartflow_prompt_builder import prompt_builder # 确认 prompt_builder 的导入路径 +from src.plugins.heartFC_chat.heartflow_prompt_builder import prompt_builder # 确认 prompt_builder 的导入路径 logger = get_logger("pfc_utils") @@ -47,33 +47,36 @@ async def retrieve_contextual_info(text: str, private_name: str) -> Tuple[str, s retrieved_memory_str = f"你回忆起:\n{related_memory_info.strip()}\n(以上是你的回忆,供参考)\n" memory_log_msg = f"自动检索到记忆: {related_memory_info.strip()[:100]}..." else: - memory_log_msg = "自动检索记忆返回为空。" + memory_log_msg = "自动检索记忆返回为空。" logger.debug(f"[私聊][{private_name}] (retrieve_contextual_info) 记忆检索: {memory_log_msg}") except Exception as e: - logger.error(f"[私聊][{private_name}] (retrieve_contextual_info) 自动检索记忆时出错: {e}\n{traceback.format_exc()}") + logger.error( + f"[私聊][{private_name}] (retrieve_contextual_info) 自动检索记忆时出错: {e}\n{traceback.format_exc()}" + ) retrieved_memory_str = "检索记忆时出错。\n" # 2. 检索知识 (逻辑来自原 action_planner 和 reply_generator) try: # 使用导入的 prompt_builder 实例及其方法 knowledge_result = await prompt_builder.get_prompt_info( - message=text, threshold=0.38 # threshold 可以根据需要调整 + message=text, + threshold=0.38, # threshold 可以根据需要调整 ) if knowledge_result: - retrieved_knowledge_str = knowledge_result # 直接使用返回结果 - knowledge_log_msg = "自动检索到相关知识。" + retrieved_knowledge_str = knowledge_result # 直接使用返回结果 + knowledge_log_msg = "自动检索到相关知识。" logger.debug(f"[私聊][{private_name}] (retrieve_contextual_info) 知识检索: {knowledge_log_msg}") except Exception as e: - logger.error(f"[私聊][{private_name}] (retrieve_contextual_info) 自动检索知识时出错: {e}\n{traceback.format_exc()}") + logger.error( + f"[私聊][{private_name}] (retrieve_contextual_info) 自动检索知识时出错: {e}\n{traceback.format_exc()}" + ) retrieved_knowledge_str = "检索知识时出错。\n" return retrieved_memory_str, retrieved_knowledge_str - - def get_items_from_json( content: str, private_name: str, diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index 1dc564de..892e881b 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -1,8 +1,10 @@ from .pfc_utils import retrieve_contextual_info + # 可能用于旧知识库提取主题 (如果需要回退到旧方法) # import jieba # 如果报错说找不到 jieba,可能需要安装: pip install jieba # import re # 正则表达式库,通常 Python 自带 from typing import Tuple, List, Dict, Any + # from src.common.logger import get_module_logger from src.common.logger_manager import get_logger from ..models.utils_model import LLMRequest @@ -114,7 +116,6 @@ class ReplyGenerator: self.chat_observer = ChatObserver.get_instance(stream_id, private_name) self.reply_checker = ReplyChecker(stream_id, private_name) - # 修改 generate 方法签名,增加 action_type 参数 async def generate( self, observation_info: ObservationInfo, conversation_info: ConversationInfo, action_type: str @@ -171,11 +172,15 @@ class ReplyGenerator: # 构建 Persona 文本 (persona_text) persona_text = f"你的名字是{self.name},{self.personality_info}。" - retrieval_context = chat_history_text # 使用前面构建好的 chat_history_text + retrieval_context = chat_history_text # 使用前面构建好的 chat_history_text # 调用共享函数进行检索 - retrieved_memory_str, retrieved_knowledge_str = await retrieve_contextual_info(retrieval_context, self.private_name) - logger.info(f"[私聊][{self.private_name}] (ReplyGenerator) 统一检索完成。记忆: {'有' if '回忆起' in retrieved_memory_str else '无'} / 知识: {'有' if '出错' not in retrieved_knowledge_str and '无相关知识' not in retrieved_knowledge_str else '无'}") - + retrieved_memory_str, retrieved_knowledge_str = await retrieve_contextual_info( + retrieval_context, self.private_name + ) + logger.info( + f"[私聊][{self.private_name}] (ReplyGenerator) 统一检索完成。记忆: {'有' if '回忆起' in retrieved_memory_str else '无'} / 知识: {'有' if '出错' not in retrieved_knowledge_str and '无相关知识' not in retrieved_knowledge_str else '无'}" + ) + # --- 修改:构建上次回复失败原因和内容提示 --- last_rejection_info_str = "" # 检查 conversation_info 是否有上次拒绝的原因和内容,并且它们都不是 None From 62b7cd6e8f8f134ef223135afd30f6d430a8f7a3 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Mon, 5 May 2025 21:36:56 +0800 Subject: [PATCH 66/67] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=9B=9E=E5=A4=8D?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/conversation.py | 628 ++++++++++++++-------------- src/plugins/PFC/observation_info.py | 181 ++++---- 2 files changed, 407 insertions(+), 402 deletions(-) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 66dbbf7b..9f4bcfa0 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +# File: conversation.py import time import asyncio import datetime @@ -9,7 +11,7 @@ from src.plugins.utils.chat_message_builder import build_readable_messages, get_ from typing import Dict, Any, Optional from ..chat.message import Message from .pfc_types import ConversationState -from .pfc import ChatObserver, GoalAnalyzer +from .pfc import ChatObserver, GoalAnalyzer # pfc.py 包含了 GoalAnalyzer,无需重复导入 from .message_sender import DirectMessageSender from src.common.logger_manager import get_logger from .action_planner import ActionPlanner @@ -19,7 +21,7 @@ from .reply_generator import ReplyGenerator from ..chat.chat_stream import ChatStream from maim_message import UserInfo from src.plugins.chat.chat_stream import chat_manager -from .pfc_KnowledgeFetcher import KnowledgeFetcher +from .pfc_KnowledgeFetcher import KnowledgeFetcher # 注意:这里是 PFC_KnowledgeFetcher.py from .waiter import Waiter import traceback @@ -114,6 +116,10 @@ class Conversation: ) # 让 ChatObserver 从加载的最后一条消息之后开始同步 + # **** 注意:这里的 last_message_time 设置可能需要 review **** + # 如果数据库消息时间戳可能不完全连续,直接设置 last_message_time 可能导致 observer 错过消息 + # 更稳妥的方式是让 observer 自己管理其内部的 last_message_time 或 last_message_id + # 暂时保留,但标记为潜在问题点。如果 observer 逻辑是可靠的,则此行 OK。 self.chat_observer.last_message_time = self.observation_info.last_message_time self.chat_observer.last_message_read = last_msg # 更新 observer 的最后读取记录 else: @@ -138,7 +144,7 @@ class Conversation: async def _plan_and_action_loop(self): """思考步,PFC核心循环模块""" while self.should_continue: - # 忽略逻辑 + # 忽略逻辑 (保持不变) if self.ignore_until_timestamp and time.time() < self.ignore_until_timestamp: await asyncio.sleep(30) continue @@ -147,64 +153,92 @@ class Conversation: self.ignore_until_timestamp = None self.should_continue = False 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 + 1 # 算上麦麦自己发的那一条 + # --- [修改点 1] 在规划前记录新消息状态 --- + # 记录规划开始时未处理消息的 ID 集合,用于后续判断哪些是新来的 + message_ids_before_planning = set() + initial_unprocessed_message_count = 0 # <-- 新增:记录规划前的未处理消息数 + if hasattr(self.observation_info, "unprocessed_messages"): + message_ids_before_planning = {msg.get("message_id") for msg in self.observation_info.unprocessed_messages if msg.get("message_id")} + initial_unprocessed_message_count = len(self.observation_info.unprocessed_messages) # <-- 获取初始数量 + logger.debug(f"[私聊][{self.private_name}]规划开始,当前未处理消息数: {initial_unprocessed_message_count}, IDs: {message_ids_before_planning}") else: logger.warning( - f"[私聊][{self.private_name}]ObservationInfo missing 'new_messages_count' before planning." + f"[私聊][{self.private_name}]ObservationInfo missing 'unprocessed_messages' before planning." ) - # --- 调用 Action Planner --- - # 传递 self.conversation_info.last_successful_reply_action + + # --- 调用 Action Planner (保持不变) --- action, reason = await self.action_planner.plan( self.observation_info, self.conversation_info, self.conversation_info.last_successful_reply_action ) - # --- 规划后检查是否有 *更多* 新消息到达 --- - current_new_message_count = 0 - if hasattr(self.observation_info, "new_messages_count"): - current_new_message_count = self.observation_info.new_messages_count + # --- [修改点 2] 规划后检查是否有 *过多* 新消息到达 --- + current_unprocessed_messages = [] + current_unprocessed_message_count = 0 + if hasattr(self.observation_info, "unprocessed_messages"): + current_unprocessed_messages = self.observation_info.unprocessed_messages + current_unprocessed_message_count = len(current_unprocessed_messages) # <-- 获取当前数量 else: logger.warning( - f"[私聊][{self.private_name}]ObservationInfo missing 'new_messages_count' after planning." + f"[私聊][{self.private_name}]ObservationInfo missing 'unprocessed_messages' after planning." ) - if current_new_message_count > initial_new_message_count + 2: + # 计算规划期间实际新增的消息数量 + new_messages_during_planning_count = 0 + new_message_ids_during_planning = set() + for msg in current_unprocessed_messages: + msg_id = msg.get("message_id") + if msg_id and msg_id not in message_ids_before_planning: + new_messages_during_planning_count += 1 + new_message_ids_during_planning.add(msg_id) + + logger.debug(f"[私聊][{self.private_name}]规划结束,当前未处理消息数: {current_unprocessed_message_count}, 规划期间新增: {new_messages_during_planning_count}") + + # **核心逻辑:判断是否中断** + # 这里的 +2 是根据你的需求来的,代表允许的缓冲 + # 我们比较的是 *规划期间新增的消息数* 是否超过阈值 + if new_messages_during_planning_count > 2: logger.info( - f"[私聊][{self.private_name}]规划期间发现新增消息 ({initial_new_message_count} -> {current_new_message_count}),跳过本次行动,重新规划" + f"[私聊][{self.private_name}]规划期间新增消息数 ({new_messages_during_planning_count}) 超过阈值(2),取消本次行动 '{action}',重新规划" ) - # 如果规划期间有新消息,也应该重置上次回复状态,因为现在要响应新消息了 + # 中断时,重置上次回复状态,因为需要基于最新消息重新决策 self.conversation_info.last_successful_reply_action = None - await asyncio.sleep(0.1) - continue + # **重要**: 中断时不清空未处理消息,留给下一轮规划处理 + await asyncio.sleep(0.1) # 短暂暂停避免CPU空转 + continue # 跳过本轮后续处理,直接进入下一轮循环重新规划 - # 包含 send_new_message - if initial_new_message_count > 0 and action in ["direct_reply", "send_new_message"]: - if hasattr(self.observation_info, "clear_unprocessed_messages"): - logger.debug( - f"[私聊][{self.private_name}]准备执行 {action},清理 {initial_new_message_count} 条规划时已知的新消息。" - ) - await self.observation_info.clear_unprocessed_messages() - if hasattr(self.observation_info, "new_messages_count"): - self.observation_info.new_messages_count = 0 - else: - logger.error( - f"[私聊][{self.private_name}]无法清理未处理消息: ObservationInfo 缺少 clear_unprocessed_messages 方法!" - ) + # --- [修改点 3] 准备执行动作,处理规划时已知的消息 --- + # 如果决定要回复 (direct_reply 或 send_new_message),并且规划开始时就有未处理消息 + # 这表示 LLM 规划时已经看到了这些消息 + # 我们需要在发送回复 *后* 清理掉这些规划时已知的消息 + # 注意:这里不再立即清理,清理逻辑移到 _handle_action 成功发送后 + messages_known_during_planning = [] + if action in ["direct_reply", "send_new_message"] and initial_unprocessed_message_count > 0: + messages_known_during_planning = [ + msg for msg_id in message_ids_before_planning + if (msg := next((m for m in self.observation_info.unprocessed_messages if m.get("message_id") == msg_id), None)) is not None + ] + logger.debug(f"[私聊][{self.private_name}]规划时已知 {len(messages_known_during_planning)} 条消息,将在回复成功后清理。") - await self._handle_action(action, reason, self.observation_info, self.conversation_info) - # 检查是否需要结束对话 (逻辑不变) + # --- 执行动作 --- + # 将规划时已知需要清理的消息ID集合传递给 _handle_action + await self._handle_action(action, reason, self.observation_info, self.conversation_info, message_ids_before_planning) + + # --- 检查是否需要结束对话 (逻辑保持不变) --- goal_ended = False if hasattr(self.conversation_info, "goal_list") and self.conversation_info.goal_list: for goal_item in self.conversation_info.goal_list: + current_goal = None # 初始化 current_goal if isinstance(goal_item, dict): current_goal = goal_item.get("goal") + elif isinstance(goal_item, str): # 处理直接是字符串的情况 + current_goal = goal_item - if current_goal == "结束对话": + # 确保 current_goal 是字符串再比较 + if isinstance(current_goal, str) and current_goal == "结束对话": goal_ended = True break @@ -215,38 +249,49 @@ class Conversation: except Exception as loop_err: logger.error(f"[私聊][{self.private_name}]PFC主循环出错: {loop_err}") logger.error(f"[私聊][{self.private_name}]{traceback.format_exc()}") - await asyncio.sleep(1) + await asyncio.sleep(1) # 出错时暂停一下 + # 循环间隔 if self.should_continue: - await asyncio.sleep(0.1) + await asyncio.sleep(0.1) # 非阻塞的短暂停 logger.info(f"[私聊][{self.private_name}]PFC 循环结束 for stream_id: {self.stream_id}") - def _check_new_messages_after_planning(self): - """检查在规划后是否有新消息""" - # 检查 ObservationInfo 是否已初始化并且有 new_messages_count 属性 - if not hasattr(self, "observation_info") or not hasattr(self.observation_info, "new_messages_count"): - logger.warning( - f"[私聊][{self.private_name}]ObservationInfo 未初始化或缺少 'new_messages_count' 属性,无法检查新消息。" - ) - return False # 或者根据需要抛出错误 - if self.observation_info.new_messages_count > 2: - logger.info( - f"[私聊][{self.private_name}]生成/执行动作期间收到 {self.observation_info.new_messages_count} 条新消息,取消当前动作并重新规划" + # --- [修改点 4] 修改 _check_new_messages_after_planning --- + # 重命名并修改逻辑,用于在 *发送前* 检查是否有过多新消息(兜底检查) + def _check_interrupt_before_sending(self, message_ids_before_planning: set) -> bool: + """在发送回复前,最后检查一次是否有过多新消息导致需要中断""" + if not hasattr(self, "observation_info") or not hasattr(self.observation_info, "unprocessed_messages"): + logger.warning( + f"[私聊][{self.private_name}]ObservationInfo 未初始化或缺少 'unprocessed_messages' 属性,无法检查新消息。" ) - # 如果有新消息,也应该重置上次回复状态 - if hasattr(self, "conversation_info"): # 确保 conversation_info 已初始化 + return False + + current_unprocessed_messages = self.observation_info.unprocessed_messages + new_messages_count = 0 + for msg in current_unprocessed_messages: + msg_id = msg.get("message_id") + if msg_id and msg_id not in message_ids_before_planning: + new_messages_count += 1 + + # 使用与规划后检查相同的阈值 + if new_messages_count > 2: + logger.info( + f"[私聊][{self.private_name}]准备发送时发现新增消息数 ({new_messages_count}) 超过阈值(2),取消发送并重新规划" + ) + if hasattr(self, "conversation_info"): self.conversation_info.last_successful_reply_action = None else: logger.warning( f"[私聊][{self.private_name}]ConversationInfo 未初始化,无法重置 last_successful_reply_action。" ) - return True - return False + return True # 需要中断 + return False # 不需要中断 + def _convert_to_message(self, msg_dict: Dict[str, Any]) -> Message: - """将消息字典转换为Message对象""" + """将消息字典转换为Message对象 (保持不变)""" try: # 尝试从 msg_dict 直接获取 chat_stream,如果失败则从全局 chat_manager 获取 chat_info = msg_dict.get("chat_info") @@ -274,8 +319,14 @@ class Conversation: # 可以选择返回 None 或重新抛出异常,这里选择重新抛出以指示问题 raise ValueError(f"无法将字典转换为 Message 对象: {e}") from e + # --- [修改点 5] 修改 _handle_action 签名并调整内部逻辑 --- async def _handle_action( - self, action: str, reason: str, observation_info: ObservationInfo, conversation_info: ConversationInfo + self, + action: str, + reason: str, + observation_info: ObservationInfo, + conversation_info: ConversationInfo, + message_ids_before_planning: set # <-- 接收规划前的消息ID集合 ): """处理规划的行动""" @@ -289,18 +340,17 @@ class Conversation: "time": datetime.datetime.now().strftime("%H:%M:%S"), "final_reason": None, } - # 确保 done_action 列表存在 if not hasattr(conversation_info, "done_action"): conversation_info.done_action = [] conversation_info.done_action.append(current_action_record) action_index = len(conversation_info.done_action) - 1 - action_successful = False # 用于标记动作是否成功完成 + action_successful = False + reply_sent = False # <-- 新增:标记是否成功发送了回复 # --- 根据不同的 action 执行 --- - - # send_new_message 失败后执行 wait - if action == "send_new_message": + if action == "direct_reply" or action == "send_new_message": + # 合并 direct_reply 和 send_new_message 的大部分逻辑 max_reply_attempts = 3 reply_attempt_count = 0 is_suitable = False @@ -310,248 +360,170 @@ class Conversation: while reply_attempt_count < max_reply_attempts and not is_suitable: reply_attempt_count += 1 - logger.info( - f"[私聊][{self.private_name}]尝试生成追问回复 (第 {reply_attempt_count}/{max_reply_attempts} 次)..." - ) + log_prefix = f"[私聊][{self.private_name}]尝试生成 '{action}' 回复 (第 {reply_attempt_count}/{max_reply_attempts} 次)..." + logger.info(log_prefix) self.state = ConversationState.GENERATING - # 1. 生成回复 (调用 generate 时传入 action_type) + # 1. 生成回复 (传入 action_type) self.generated_reply = await self.reply_generator.generate( - observation_info, conversation_info, action_type="send_new_message" - ) - logger.info( - f"[私聊][{self.private_name}]第 {reply_attempt_count} 次生成的追问回复: {self.generated_reply}" + observation_info, conversation_info, action_type=action ) + logger.info(f"{log_prefix} 生成内容: {self.generated_reply}") # 2. 检查回复 (逻辑不变) self.state = ConversationState.CHECKING try: - current_goal_str = conversation_info.goal_list[0]["goal"] if conversation_info.goal_list else "" + current_goal_str = "" # 初始化 + if hasattr(conversation_info, 'goal_list') and conversation_info.goal_list: + goal_item = conversation_info.goal_list[0] # 取第一个目标 + if isinstance(goal_item, dict): + current_goal_str = goal_item.get('goal', '') + elif isinstance(goal_item, str): + current_goal_str = goal_item + + # 确保 chat_history 和 chat_history_str 存在 + chat_history_for_check = getattr(observation_info, 'chat_history', []) + chat_history_str_for_check = getattr(observation_info, 'chat_history_str', '') + 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, - chat_history_str=observation_info.chat_history_str, + chat_history=chat_history_for_check, + chat_history_str=chat_history_str_for_check, retry_count=reply_attempt_count - 1, ) logger.info( - f"[私聊][{self.private_name}]第 {reply_attempt_count} 次追问检查结果: 合适={is_suitable}, 原因='{check_reason}', 需重新规划={need_replan}" + f"{log_prefix} 检查结果: 合适={is_suitable}, 原因='{check_reason}', 需重新规划={need_replan}" ) + + # 更新拒绝原因和内容 (仅在不合适或需要重规划时) if not is_suitable or need_replan: conversation_info.last_reply_rejection_reason = check_reason conversation_info.last_rejected_reply_content = self.generated_reply else: - # 如果检查通过,清空上次的拒绝原因 + # 检查通过,清空上次拒绝记录 conversation_info.last_reply_rejection_reason = None conversation_info.last_rejected_reply_content = None if is_suitable: final_reply_to_send = self.generated_reply - break + break # 检查通过,跳出循环 elif need_replan: logger.warning( - f"[私聊][{self.private_name}]第 {reply_attempt_count} 次追问检查建议重新规划,停止尝试。原因: {check_reason}" + f"{log_prefix} 检查建议重新规划,停止尝试。原因: {check_reason}" ) - break + break # 需要重新规划,跳出循环 except Exception as check_err: logger.error( - f"[私聊][{self.private_name}]第 {reply_attempt_count} 次调用 ReplyChecker (追问) 时出错: {check_err}" + f"{log_prefix} 调用 ReplyChecker 时出错: {check_err}" ) check_reason = f"第 {reply_attempt_count} 次检查过程出错: {check_err}" - conversation_info.last_reply_rejection_reason = f"检查过程出错: {check_err}" # 出错也记录原因 - conversation_info.last_rejected_reply_content = self.generated_reply - break + conversation_info.last_reply_rejection_reason = f"检查过程出错: {check_err}" + conversation_info.last_rejected_reply_content = self.generated_reply # 记录出错时尝试的内容 + break # 检查出错,跳出循环 - # 循环结束,处理最终结果 + # --- 处理生成和检查的结果 --- if is_suitable: - # 检查是否有新消息 - if self._check_new_messages_after_planning(): - logger.info(f"[私聊][{self.private_name}]生成追问回复期间收到新消息,取消发送,重新规划行动") + # --- [修改点 6] 发送前最后检查是否需要中断 --- + if self._check_interrupt_before_sending(message_ids_before_planning): + logger.info(f"[私聊][{self.private_name}]生成回复后、发送前发现过多新消息,取消发送,重新规划") conversation_info.done_action[action_index].update( - {"status": "recall", "final_reason": f"有新消息,取消发送追问: {final_reply_to_send}"} + {"status": "recall", "final_reason": f"发送前发现过多新消息,取消发送: {final_reply_to_send}"} ) - return # 直接返回,重新规划 + self.conversation_info.last_successful_reply_action = None # 重置状态 + return # 直接返回,主循环会重新规划 - # 发送合适的回复 + # 确认发送 self.generated_reply = final_reply_to_send - # --- 在这里调用 _send_reply --- - await self._send_reply() # <--- 调用恢复后的函数 - # --- 新增:回复成功,清除拒绝原因 --- - conversation_info.last_reply_rejection_reason = None - conversation_info.last_rejected_reply_content = None - # 更新状态: 标记上次成功是 send_new_message - self.conversation_info.last_successful_reply_action = "send_new_message" - action_successful = True # 标记动作成功 + send_success = await self._send_reply() # 调用发送函数 + + if send_success: + action_successful = True + reply_sent = True # 标记回复已发送 + logger.info(f"[私聊][{self.private_name}]成功发送 '{action}' 回复.") + # 清空上次拒绝记录 (再次确保) + conversation_info.last_reply_rejection_reason = None + conversation_info.last_rejected_reply_content = None + + # --- [修改点 7] 发送成功后,处理新消息并决定下一轮 prompt 类型 --- + # 获取发送后的最新未处理消息列表 + final_unprocessed_messages = getattr(observation_info, 'unprocessed_messages', []) + final_unprocessed_count = len(final_unprocessed_messages) + + # 计算在生成和发送期间新增的消息数 + new_messages_during_generation_count = 0 + for msg in final_unprocessed_messages: + msg_id = msg.get("message_id") + # 如果消息 ID 不在规划前的集合中,说明是新来的 + if msg_id and msg_id not in message_ids_before_planning: + new_messages_during_generation_count += 1 + + logger.debug(f"[私聊][{self.private_name}]回复发送后,当前未处理消息数: {final_unprocessed_count}, 其中生成/发送期间新增: {new_messages_during_generation_count}") + + # 根据生成期间是否有新消息,决定下次规划用哪个 prompt + if new_messages_during_generation_count > 0: + # 有 1 条或更多新消息在生成期间到达 + logger.info(f"[私聊][{self.private_name}]检测到 {new_messages_during_generation_count} 条在生成/发送期间到达的新消息,下一轮将使用首次回复逻辑处理。") + self.conversation_info.last_successful_reply_action = None # 强制下一轮用 PROMPT_INITIAL_REPLY + else: + # 没有新消息在生成期间到达 + logger.info(f"[私聊][{self.private_name}]生成/发送期间无新消息,下一轮将根据 '{action}' 使用追问逻辑。") + self.conversation_info.last_successful_reply_action = action # 保持状态,下一轮可能用 PROMPT_FOLLOW_UP + + # --- [修改点 8] 清理规划时已知的消息 --- + # 只有在回复成功发送后,才清理掉那些在规划时就已经看到的消息 + if message_ids_before_planning: + await observation_info.clear_processed_messages(message_ids_before_planning) + + + else: # 发送失败 + logger.error(f"[私聊][{self.private_name}]发送 '{action}' 回复失败。") + # 发送失败,也认为动作未成功,重置状态 + action_successful = False + self.conversation_info.last_successful_reply_action = None + conversation_info.done_action[action_index].update( + {"status": "recall", "final_reason": "发送回复时失败"} + ) elif need_replan: - # 打回动作决策 - logger.warning( - f"[私聊][{self.private_name}]经过 {reply_attempt_count} 次尝试,追问回复决定打回动作决策。打回原因: {check_reason}" - ) - conversation_info.done_action[action_index].update( - {"status": "recall", "final_reason": f"追问尝试{reply_attempt_count}次后打回: {check_reason}"} - ) + # 检查后决定打回动作决策 + logger.warning( + f"[私聊][{self.private_name}]'{action}' 回复检查后决定打回动作决策 (尝试 {reply_attempt_count} 次)。打回原因: {check_reason}" + ) + conversation_info.done_action[action_index].update( + {"status": "recall", "final_reason": f"'{action}' 尝试{reply_attempt_count}次后打回: {check_reason}"} + ) + self.conversation_info.last_successful_reply_action = None # 重置状态 - else: - # 追问失败 - logger.warning( - f"[私聊][{self.private_name}]经过 {reply_attempt_count} 次尝试,未能生成合适的追问回复。最终原因: {check_reason}" - ) - conversation_info.done_action[action_index].update( - {"status": "recall", "final_reason": f"追问尝试{reply_attempt_count}次后失败: {check_reason}"} - ) - # 重置状态: 追问失败,下次用初始 prompt - self.conversation_info.last_successful_reply_action = None + else: # 多次尝试后仍然不合适 (is_suitable is False and not need_replan) + logger.warning( + f"[私聊][{self.private_name}]经过 {reply_attempt_count} 次尝试,未能生成合适的 '{action}' 回复。最终原因: {check_reason}" + ) + conversation_info.done_action[action_index].update( + {"status": "recall", "final_reason": f"'{action}' 尝试{reply_attempt_count}次后失败: {check_reason}"} + ) + self.conversation_info.last_successful_reply_action = None # 重置状态 - # 执行 Wait 操作 - logger.info(f"[私聊][{self.private_name}]由于无法生成合适追问回复,执行 'wait' 操作...") - self.state = ConversationState.WAITING - await self.waiter.wait(self.conversation_info) - wait_action_record = { - "action": "wait", - "plan_reason": "因 send_new_message 多次尝试失败而执行的后备等待", - "status": "done", - "time": datetime.datetime.now().strftime("%H:%M:%S"), - "final_reason": None, - } - conversation_info.done_action.append(wait_action_record) - - elif action == "direct_reply": - max_reply_attempts = 3 - reply_attempt_count = 0 - is_suitable = False - need_replan = False - check_reason = "未进行尝试" - final_reply_to_send = "" - - while reply_attempt_count < max_reply_attempts and not is_suitable: - reply_attempt_count += 1 - logger.info( - f"[私聊][{self.private_name}]尝试生成首次回复 (第 {reply_attempt_count}/{max_reply_attempts} 次)..." - ) - self.state = ConversationState.GENERATING - - # 1. 生成回复 - self.generated_reply = await self.reply_generator.generate( - observation_info, conversation_info, action_type="direct_reply" - ) - logger.info( - f"[私聊][{self.private_name}]第 {reply_attempt_count} 次生成的首次回复: {self.generated_reply}" - ) - - # 2. 检查回复 - self.state = ConversationState.CHECKING - try: - current_goal_str = conversation_info.goal_list[0]["goal"] if conversation_info.goal_list else "" - 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, - chat_history_str=observation_info.chat_history_str, - retry_count=reply_attempt_count - 1, - ) - logger.info( - f"[私聊][{self.private_name}]第 {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"[私聊][{self.private_name}]第 {reply_attempt_count} 次首次回复检查建议重新规划,停止尝试。原因: {check_reason}" - ) - break - except Exception as check_err: - logger.error( - f"[私聊][{self.private_name}]第 {reply_attempt_count} 次调用 ReplyChecker (首次回复) 时出错: {check_err}" - ) - check_reason = f"第 {reply_attempt_count} 次检查过程出错: {check_err}" - break - - # 循环结束,处理最终结果 - if is_suitable: - # 检查是否有新消息 - if self._check_new_messages_after_planning(): - logger.info(f"[私聊][{self.private_name}]生成首次回复期间收到新消息,取消发送,重新规划行动") - conversation_info.done_action[action_index].update( - {"status": "recall", "final_reason": f"有新消息,取消发送首次回复: {final_reply_to_send}"} - ) - return # 直接返回,重新规划 - - # 发送合适的回复 - self.generated_reply = final_reply_to_send - # --- 在这里调用 _send_reply --- - await self._send_reply() # <--- 调用恢复后的函数 - # --- 新增:回复成功,清除拒绝原因 --- - conversation_info.last_reply_rejection_reason = None - conversation_info.last_rejected_reply_content = None # <-- 新增清空内容 - # 更新状态: 标记上次成功是 direct_reply - self.conversation_info.last_successful_reply_action = "direct_reply" - action_successful = True # 标记动作成功 - - elif need_replan: - # 打回动作决策 - logger.warning( - f"[私聊][{self.private_name}]经过 {reply_attempt_count} 次尝试,首次回复决定打回动作决策。打回原因: {check_reason}" - ) - conversation_info.done_action[action_index].update( - {"status": "recall", "final_reason": f"首次回复尝试{reply_attempt_count}次后打回: {check_reason}"} - ) - - else: - # 首次回复失败 - logger.warning( - f"[私聊][{self.private_name}]经过 {reply_attempt_count} 次尝试,未能生成合适的首次回复。最终原因: {check_reason}" - ) - conversation_info.done_action[action_index].update( - {"status": "recall", "final_reason": f"首次回复尝试{reply_attempt_count}次后失败: {check_reason}"} - ) - # 重置状态: 首次回复失败,下次还是用初始 prompt - self.conversation_info.last_successful_reply_action = None - - # 执行 Wait 操作 (保持原有逻辑) - logger.info(f"[私聊][{self.private_name}]由于无法生成合适首次回复,执行 'wait' 操作...") - self.state = ConversationState.WAITING - await self.waiter.wait(self.conversation_info) - wait_action_record = { - "action": "wait", - "plan_reason": "因 direct_reply 多次尝试失败而执行的后备等待", - "status": "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.state = ConversationState.FETCHING - # knowledge_query = reason - # try: - # 检查 knowledge_fetcher 是否存在 - # if not hasattr(self, "knowledge_fetcher"): - # logger.error(f"[私聊][{self.private_name}]KnowledgeFetcher 未初始化,无法获取知识。") - # raise AttributeError("KnowledgeFetcher not initialized") - - # knowledge, source = await self.knowledge_fetcher.fetch(knowledge_query, observation_info.chat_history) - # logger.info(f"[私聊][{self.private_name}]获取到知识: {knowledge[:100]}..., 来源: {source}") - # if knowledge: - # 确保 knowledge_list 存在 - # if not hasattr(conversation_info, "knowledge_list"): - # conversation_info.knowledge_list = [] - # conversation_info.knowledge_list.append( - # {"query": knowledge_query, "knowledge": knowledge, "source": source} - # ) - # action_successful = True - # except Exception as fetch_err: - # logger.error(f"[私聊][{self.private_name}]获取知识时出错: {str(fetch_err)}") - # conversation_info.done_action[action_index].update( - # {"status": "recall", "final_reason": f"获取知识失败: {str(fetch_err)}"} - # ) - # self.conversation_info.last_successful_reply_action = None # 重置状态 + # 如果是 send_new_message 失败,则执行 wait (保持原 fallback 逻辑) + if action == "send_new_message": + logger.info(f"[私聊][{self.private_name}]由于无法生成合适追问回复,执行 'wait' 操作...") + self.state = ConversationState.WAITING + await self.waiter.wait(self.conversation_info) + wait_action_record = { + "action": "wait", + "plan_reason": "因 send_new_message 多次尝试失败而执行的后备等待", + "status": "done", # wait 本身算完成 + "time": datetime.datetime.now().strftime("%H:%M:%S"), + "final_reason": None, + } + conversation_info.done_action.append(wait_action_record) + action_successful = True # fallback wait 成功 + # 注意: fallback wait 成功后,last_successful_reply_action 仍然是 None + # --- 处理其他动作 (保持大部分不变,主要是确保状态重置) --- elif action == "rethink_goal": self.state = ConversationState.RETHINKING try: - # 检查 goal_analyzer 是否存在 if not hasattr(self, "goal_analyzer"): logger.error(f"[私聊][{self.private_name}]GoalAnalyzer 未初始化,无法重新思考目标。") raise AttributeError("GoalAnalyzer not initialized") @@ -562,67 +534,99 @@ class Conversation: conversation_info.done_action[action_index].update( {"status": "recall", "final_reason": f"重新思考目标失败: {rethink_err}"} ) - self.conversation_info.last_successful_reply_action = None # 重置状态 + # 无论成功失败,非回复动作都重置 last_successful_reply_action + self.conversation_info.last_successful_reply_action = None + conversation_info.last_reply_rejection_reason = None # 清除拒绝原因 + conversation_info.last_rejected_reply_content = None + elif action == "listening": self.state = ConversationState.LISTENING logger.info(f"[私聊][{self.private_name}]倾听对方发言...") try: - # 检查 waiter 是否存在 if not hasattr(self, "waiter"): logger.error(f"[私聊][{self.private_name}]Waiter 未初始化,无法倾听。") raise AttributeError("Waiter not initialized") await self.waiter.wait_listening(conversation_info) - action_successful = True # Listening 完成就算成功 + action_successful = True except Exception as listen_err: logger.error(f"[私聊][{self.private_name}]倾听时出错: {listen_err}") conversation_info.done_action[action_index].update( {"status": "recall", "final_reason": f"倾听失败: {listen_err}"} ) - self.conversation_info.last_successful_reply_action = None # 重置状态 + # 无论成功失败,非回复动作都重置 + self.conversation_info.last_successful_reply_action = None + conversation_info.last_reply_rejection_reason = None + conversation_info.last_rejected_reply_content = None elif action == "say_goodbye": - self.state = ConversationState.GENERATING # 也可以定义一个新的状态,如 ENDING + self.state = ConversationState.GENERATING logger.info(f"[私聊][{self.private_name}]执行行动: 生成并发送告别语...") try: - # 1. 生成告别语 (使用 'say_goodbye' action_type) + # 1. 生成告别语 self.generated_reply = await self.reply_generator.generate( observation_info, conversation_info, action_type="say_goodbye" ) logger.info(f"[私聊][{self.private_name}]生成的告别语: {self.generated_reply}") - # 2. 直接发送告别语 (不经过检查) - if self.generated_reply: # 确保生成了内容 - await self._send_reply() # 调用发送方法 - # 发送成功后,标记动作成功 - action_successful = True - logger.info(f"[私聊][{self.private_name}]告别语已发送。") + # 2. 发送告别语 + if self.generated_reply: + # --- [修改点 9] 告别前也检查中断 --- + if self._check_interrupt_before_sending(message_ids_before_planning): + logger.info(f"[私聊][{self.private_name}]发送告别语前发现过多新消息,取消发送,重新规划") + conversation_info.done_action[action_index].update( + {"status": "recall", "final_reason": "发送告别语前发现过多新消息"} + ) + self.should_continue = True # 不能结束,需要重规划 + self.conversation_info.last_successful_reply_action = None # 重置状态 + return + + send_success = await self._send_reply() + if send_success: + action_successful = True + reply_sent = True # 标记发送成功 + logger.info(f"[私聊][{self.private_name}]告别语已发送。") + # 发送告别语成功后,通常意味着对话结束 + self.should_continue = False + logger.info(f"[私聊][{self.private_name}]发送告别语流程结束,即将停止对话实例。") + else: + logger.warning(f"[私聊][{self.private_name}]发送告别语失败。") + action_successful = False + # 发送失败不应结束对话,可能需要重试或做其他事 + self.should_continue = True + conversation_info.done_action[action_index].update( + {"status": "recall", "final_reason": "发送告别语失败"} + ) + self.conversation_info.last_successful_reply_action = None # 重置状态 + else: logger.warning(f"[私聊][{self.private_name}]未能生成告别语内容,无法发送。") - action_successful = False # 标记动作失败 + action_successful = False + self.should_continue = True # 未能生成也不能结束 conversation_info.done_action[action_index].update( {"status": "recall", "final_reason": "未能生成告别语内容"} ) - - # 3. 无论是否发送成功,都准备结束对话 - self.should_continue = False - logger.info(f"[私聊][{self.private_name}]发送告别语流程结束,即将停止对话实例。") + self.conversation_info.last_successful_reply_action = None except Exception as goodbye_err: logger.error(f"[私聊][{self.private_name}]生成或发送告别语时出错: {goodbye_err}") logger.error(f"[私聊][{self.private_name}]{traceback.format_exc()}") - # 即使出错,也结束对话 - self.should_continue = False - action_successful = False # 标记动作失败 + action_successful = False + self.should_continue = True # 出错也不能结束 conversation_info.done_action[action_index].update( {"status": "recall", "final_reason": f"生成或发送告别语时出错: {goodbye_err}"} ) + self.conversation_info.last_successful_reply_action = None elif action == "end_conversation": - # 这个分支现在只会在 action_planner 最终决定不告别时被调用 self.should_continue = False logger.info(f"[私聊][{self.private_name}]收到最终结束指令,停止对话...") - action_successful = True # 标记这个指令本身是成功的 + action_successful = True + # 结束对话也重置状态 + self.conversation_info.last_successful_reply_action = None + conversation_info.last_reply_rejection_reason = None + conversation_info.last_rejected_reply_content = None + elif action == "block_and_ignore": logger.info(f"[私聊][{self.private_name}]不想再理你了...") @@ -632,27 +636,34 @@ class Conversation: f"[私聊][{self.private_name}]将忽略此对话直到: {datetime.datetime.fromtimestamp(self.ignore_until_timestamp)}" ) self.state = ConversationState.IGNORED - action_successful = True # 标记动作成功 + action_successful = True + # 忽略也重置状态 + self.conversation_info.last_successful_reply_action = None + conversation_info.last_reply_rejection_reason = None + conversation_info.last_rejected_reply_content = None + else: # 对应 'wait' 动作 self.state = ConversationState.WAITING logger.info(f"[私聊][{self.private_name}]等待更多信息...") try: - # 检查 waiter 是否存在 if not hasattr(self, "waiter"): logger.error(f"[私聊][{self.private_name}]Waiter 未初始化,无法等待。") raise AttributeError("Waiter not initialized") _timeout_occurred = await self.waiter.wait(self.conversation_info) - action_successful = True # Wait 完成就算成功 + action_successful = True except Exception as wait_err: logger.error(f"[私聊][{self.private_name}]等待时出错: {wait_err}") conversation_info.done_action[action_index].update( {"status": "recall", "final_reason": f"等待失败: {wait_err}"} ) - self.conversation_info.last_successful_reply_action = None # 重置状态 + # 无论成功失败,非回复动作都重置 + self.conversation_info.last_successful_reply_action = None + conversation_info.last_reply_rejection_reason = None + conversation_info.last_rejected_reply_content = None + # --- 更新 Action History 状态 --- - # 只有当动作本身成功时,才更新状态为 done if action_successful: conversation_info.done_action[action_index].update( { @@ -660,51 +671,50 @@ class Conversation: "time": datetime.datetime.now().strftime("%H:%M:%S"), } ) - # 重置状态: 对于非回复类动作的成功,清除上次回复状态 - if action not in ["direct_reply", "send_new_message"]: - self.conversation_info.last_successful_reply_action = None - # --- 新增:非回复动作成功,也清除拒绝原因 --- - conversation_info.last_reply_rejection_reason = None - conversation_info.last_rejected_reply_content = None # <-- 新增清空内容 - logger.debug(f"[私聊][{self.private_name}]动作 {action} 成功完成,重置 last_successful_reply_action") - # 如果动作是 recall 状态,在各自的处理逻辑中已经更新了 done_action + # **注意**: last_successful_reply_action 的更新逻辑已经移到各自的动作处理中 + logger.debug(f"[私聊][{self.private_name}]动作 '{action}' 标记为 'done'") + else: + # 如果动作是 recall 状态,在各自的处理逻辑中已经更新了 done_action 的 final_reason + logger.debug(f"[私聊][{self.private_name}]动作 '{action}' 标记为 'recall' 或失败") - async def _send_reply(self): - """发送回复""" + # --- [修改点 10] _send_reply 返回布尔值表示成功与否 --- + async def _send_reply(self) -> bool: + """发送回复,并返回是否发送成功""" if not self.generated_reply: logger.warning(f"[私聊][{self.private_name}]没有生成回复内容,无法发送。") - return + return False # 发送失败 try: - _current_time = time.time() reply_content = self.generated_reply - # 发送消息 (确保 direct_sender 和 chat_stream 有效) + # 检查依赖项 if not hasattr(self, "direct_sender") or not self.direct_sender: logger.error(f"[私聊][{self.private_name}]DirectMessageSender 未初始化,无法发送回复。") - return + return False # 发送失败 if not self.chat_stream: logger.error(f"[私聊][{self.private_name}]ChatStream 未初始化,无法发送回复。") - return + return False # 发送失败 + # 发送消息 await self.direct_sender.send_message(chat_stream=self.chat_stream, content=reply_content) - # 发送成功后,手动触发 observer 更新可能导致重复处理自己发送的消息 - # 更好的做法是依赖 observer 的自动轮询或数据库触发器(如果支持) - # 暂时注释掉,观察是否影响 ObservationInfo 的更新 + # 发送成功后,可以考虑触发 observer 更新,但需谨慎避免竞争条件或重复处理 + # 暂时注释掉,依赖 observer 的自然更新周期 # self.chat_observer.trigger_update() - # if not await self.chat_observer.wait_for_update(): - # logger.warning(f"[私聊][{self.private_name}]等待 ChatObserver 更新完成超时") + # await self.chat_observer.wait_for_update() - self.state = ConversationState.ANALYZING # 更新状态 + self.state = ConversationState.ANALYZING # 更新状态 (例如,可以改为 IDLE 或 WAITING) + return True # 发送成功 except Exception as e: - logger.error(f"[私聊][{self.private_name}]发送消息或更新状态时失败: {str(e)}") + logger.error(f"[私聊][{self.private_name}]发送消息时失败: {str(e)}") logger.error(f"[私聊][{self.private_name}]{traceback.format_exc()}") - self.state = ConversationState.ANALYZING + self.state = ConversationState.ANALYZING # 或者设置为 ERROR 状态? + return False # 发送失败 + async def _send_timeout_message(self): - """发送超时结束消息""" + """发送超时结束消息 (保持不变)""" try: messages = self.chat_observer.get_cached_messages(limit=1) if not messages: @@ -715,4 +725,4 @@ class Conversation: chat_stream=self.chat_stream, content="TODO:超时消息", reply_to_message=latest_message ) except Exception as e: - logger.error(f"[私聊][{self.private_name}]发送超时消息失败: {str(e)}") + logger.error(f"[私聊][{self.private_name}]发送超时消息失败: {str(e)}") \ No newline at end of file diff --git a/src/plugins/PFC/observation_info.py b/src/plugins/PFC/observation_info.py index c7572955..35c63741 100644 --- a/src/plugins/PFC/observation_info.py +++ b/src/plugins/PFC/observation_info.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +# File: observation_info.py from typing import List, Optional, Dict, Any, Set from maim_message import UserInfo import time @@ -95,8 +97,11 @@ class ObservationInfoHandler(NotificationHandler): self.observation_info.unprocessed_messages = [ msg for msg in self.observation_info.unprocessed_messages if msg.get("message_id") != message_id ] - if len(self.observation_info.unprocessed_messages) < original_count: - logger.info(f"[私聊][{self.private_name}]移除了未处理的消息 (ID: {message_id})") + # --- [修改点 11] 更新 new_messages_count --- + self.observation_info.new_messages_count = len(self.observation_info.unprocessed_messages) + if self.observation_info.new_messages_count < original_count: + logger.info(f"[私聊][{self.private_name}]移除了未处理的消息 (ID: {message_id}), 当前未处理数: {self.observation_info.new_messages_count}") + elif notification_type == NotificationType.USER_JOINED: # 处理用户加入通知 (如果适用私聊场景) @@ -168,7 +173,7 @@ class ObservationInfo: self.last_message_id: Optional[str] = None self.last_message_content: str = "" self.last_message_sender: Optional[str] = None - self.bot_id: Optional[str] = None + self.bot_id: Optional[str] = None # 需要在某个地方设置 bot_id,例如从 global_config 获取 self.chat_history_count: int = 0 self.new_messages_count: int = 0 self.cold_chat_start_time: Optional[float] = None @@ -184,44 +189,43 @@ class ObservationInfo: self.handler: ObservationInfoHandler = ObservationInfoHandler(self, self.private_name) - def bind_to_chat_observer(self, chat_observer: ChatObserver): - """绑定到指定的chat_observer + # --- 初始化 bot_id --- + from ...config.config import global_config # 移动到 __init__ 内部以避免循环导入问题 + self.bot_id = str(global_config.BOT_QQ) if global_config.BOT_QQ else None - Args: - chat_observer: 要绑定的 ChatObserver 实例 - """ + def bind_to_chat_observer(self, chat_observer: ChatObserver): + """绑定到指定的chat_observer (保持不变)""" if self.chat_observer: logger.warning(f"[私聊][{self.private_name}]尝试重复绑定 ChatObserver") return self.chat_observer = chat_observer try: - if not self.handler: # 确保 handler 已经被创建 + if not self.handler: logger.error(f"[私聊][{self.private_name}] 尝试绑定时 handler 未初始化!") - self.chat_observer = None # 重置,防止后续错误 + self.chat_observer = None return - # 注册关心的通知类型 self.chat_observer.notification_manager.register_handler( target="observation_info", notification_type=NotificationType.NEW_MESSAGE, handler=self.handler ) self.chat_observer.notification_manager.register_handler( target="observation_info", notification_type=NotificationType.COLD_CHAT, handler=self.handler ) - # 可以根据需要注册更多通知类型 - # self.chat_observer.notification_manager.register_handler( - # target="observation_info", notification_type=NotificationType.MESSAGE_DELETED, handler=self.handler - # ) + # --- [修改点 12] 注册 MESSAGE_DELETED --- + self.chat_observer.notification_manager.register_handler( + target="observation_info", notification_type=NotificationType.MESSAGE_DELETED, handler=self.handler + ) logger.info(f"[私聊][{self.private_name}]成功绑定到 ChatObserver") except Exception as e: logger.error(f"[私聊][{self.private_name}]绑定到 ChatObserver 时出错: {e}") - self.chat_observer = None # 绑定失败,重置 + self.chat_observer = None def unbind_from_chat_observer(self): - """解除与chat_observer的绑定""" + """解除与chat_observer的绑定 (保持不变)""" if ( self.chat_observer and hasattr(self.chat_observer, "notification_manager") and self.handler - ): # 增加 handler 检查 + ): try: self.chat_observer.notification_manager.unregister_handler( target="observation_info", notification_type=NotificationType.NEW_MESSAGE, handler=self.handler @@ -229,161 +233,152 @@ class ObservationInfo: self.chat_observer.notification_manager.unregister_handler( target="observation_info", notification_type=NotificationType.COLD_CHAT, handler=self.handler ) - # 如果注册了其他类型,也要在这里注销 - # self.chat_observer.notification_manager.unregister_handler( - # target="observation_info", notification_type=NotificationType.MESSAGE_DELETED, handler=self.handler - # ) + # --- [修改点 13] 注销 MESSAGE_DELETED --- + self.chat_observer.notification_manager.unregister_handler( + target="observation_info", notification_type=NotificationType.MESSAGE_DELETED, handler=self.handler + ) logger.info(f"[私聊][{self.private_name}]成功从 ChatObserver 解绑") except Exception as e: logger.error(f"[私聊][{self.private_name}]从 ChatObserver 解绑时出错: {e}") - finally: # 确保 chat_observer 被重置 + finally: self.chat_observer = None else: logger.warning(f"[私聊][{self.private_name}]尝试解绑时 ChatObserver 不存在、无效或 handler 未设置") - # 修改:update_from_message 接收 UserInfo 对象 async def update_from_message(self, message: Dict[str, Any], user_info: Optional[UserInfo]): - """从消息更新信息 - - Args: - message: 消息数据字典 - user_info: 解析后的 UserInfo 对象 (可能为 None) - """ + """从消息更新信息 (保持不变)""" message_time = message.get("time") message_id = message.get("message_id") processed_text = message.get("processed_plain_text", "") - # 只有在新消息到达时才更新 last_message 相关信息 if message_time and message_time > (self.last_message_time or 0): self.last_message_time = message_time self.last_message_id = message_id self.last_message_content = processed_text - # 重置冷场计时器 self.is_cold_chat = False self.cold_chat_start_time = None self.cold_chat_duration = 0.0 if user_info: - sender_id = str(user_info.user_id) # 确保是字符串 + sender_id = str(user_info.user_id) self.last_message_sender = sender_id - # 更新发言时间 if sender_id == self.bot_id: self.last_bot_speak_time = message_time else: self.last_user_speak_time = message_time - self.active_users.add(sender_id) # 用户发言则认为其活跃 + self.active_users.add(sender_id) else: logger.warning( f"[私聊][{self.private_name}]处理消息更新时缺少有效的 UserInfo 对象, message_id: {message_id}" ) - self.last_message_sender = None # 发送者未知 + self.last_message_sender = None - # 将原始消息字典添加到未处理列表 - self.unprocessed_messages.append(message) - self.new_messages_count = len(self.unprocessed_messages) # 直接用列表长度 + # --- [修改点 14] 添加到未处理列表,并更新计数 --- + # 检查消息是否已存在于未处理列表中,避免重复添加 + if not any(msg.get("message_id") == message_id for msg in self.unprocessed_messages): + self.unprocessed_messages.append(message) + self.new_messages_count = len(self.unprocessed_messages) + logger.debug(f"[私聊][{self.private_name}]添加新未处理消息 ID: {message_id}, 当前未处理数: {self.new_messages_count}") + self.update_changed() + else: + logger.warning(f"[私聊][{self.private_name}]尝试重复添加未处理消息 ID: {message_id}") - # logger.debug(f"[私聊][{self.private_name}]消息更新: last_time={self.last_message_time}, new_count={self.new_messages_count}") - self.update_changed() # 标记状态已改变 else: - # 如果消息时间戳不是最新的,可能不需要处理,或者记录一个警告 pass - # logger.warning(f"[私聊][{self.private_name}]收到过时或无效时间戳的消息: ID={message_id}, time={message_time}") + def update_changed(self): - """标记状态已改变,并重置标记""" - # logger.debug(f"[私聊][{self.private_name}]状态标记为已改变 (changed=True)") + """标记状态已改变,并重置标记 (保持不变)""" self.changed = True async def update_cold_chat_status(self, is_cold: bool, current_time: float): - """更新冷场状态 - - Args: - is_cold: 是否处于冷场状态 - current_time: 当前时间戳 - """ - if is_cold != self.is_cold_chat: # 仅在状态变化时更新 + """更新冷场状态 (保持不变)""" + if is_cold != self.is_cold_chat: self.is_cold_chat = is_cold if is_cold: - # 进入冷场状态 self.cold_chat_start_time = ( self.last_message_time or current_time - ) # 从最后消息时间开始算,或从当前时间开始 + ) logger.info(f"[私聊][{self.private_name}]进入冷场状态,开始时间: {self.cold_chat_start_time}") else: - # 结束冷场状态 if self.cold_chat_start_time: self.cold_chat_duration = current_time - self.cold_chat_start_time logger.info(f"[私聊][{self.private_name}]结束冷场状态,持续时间: {self.cold_chat_duration:.2f} 秒") - self.cold_chat_start_time = None # 重置开始时间 - self.update_changed() # 状态变化,标记改变 + self.cold_chat_start_time = None + self.update_changed() - # 即使状态没变,如果是冷场状态,也更新持续时间 if self.is_cold_chat and self.cold_chat_start_time: self.cold_chat_duration = current_time - self.cold_chat_start_time def get_active_duration(self) -> float: - """获取当前活跃时长 (距离最后一条消息的时间) - - Returns: - float: 最后一条消息到现在的时长(秒) - """ + """获取当前活跃时长 (保持不变)""" if not self.last_message_time: return 0.0 return time.time() - self.last_message_time def get_user_response_time(self) -> Optional[float]: - """获取用户最后响应时间 (距离用户最后发言的时间) - - Returns: - Optional[float]: 用户最后发言到现在的时长(秒),如果没有用户发言则返回None - """ + """获取用户最后响应时间 (保持不变)""" if not self.last_user_speak_time: return None return time.time() - self.last_user_speak_time def get_bot_response_time(self) -> Optional[float]: - """获取机器人最后响应时间 (距离机器人最后发言的时间) - - Returns: - Optional[float]: 机器人最后发言到现在的时长(秒),如果没有机器人发言则返回None - """ + """获取机器人最后响应时间 (保持不变)""" if not self.last_bot_speak_time: return None return time.time() - self.last_bot_speak_time - async def clear_unprocessed_messages(self): - """将未处理消息移入历史记录,并更新相关状态""" - if not self.unprocessed_messages: - return # 没有未处理消息,直接返回 + # --- [修改点 15] 重命名并修改 clear_unprocessed_messages --- + # async def clear_unprocessed_messages(self): <-- 旧方法注释掉或删除 + async def clear_processed_messages(self, message_ids_to_clear: Set[str]): + """将指定ID的未处理消息移入历史记录,并更新相关状态""" + if not message_ids_to_clear: + logger.debug(f"[私聊][{self.private_name}]没有需要清理的消息 ID。") + return - # logger.debug(f"[私聊][{self.private_name}]处理 {len(self.unprocessed_messages)} 条未处理消息...") - # 将未处理消息添加到历史记录中 (确保历史记录有长度限制,避免无限增长) - max_history_len = 100 # 示例:最多保留100条历史记录 - self.chat_history.extend(self.unprocessed_messages) + messages_to_move = [] + remaining_messages = [] + cleared_count = 0 + + # 分离要清理和要保留的消息 + for msg in self.unprocessed_messages: + if msg.get("message_id") in message_ids_to_clear: + messages_to_move.append(msg) + cleared_count += 1 + else: + remaining_messages.append(msg) + + if not messages_to_move: + logger.debug(f"[私聊][{self.private_name}]未找到与 ID 列表匹配的未处理消息进行清理。") + return + + logger.debug(f"[私聊][{self.private_name}]准备清理 {cleared_count} 条已处理消息...") + + # 将要移动的消息添加到历史记录 + max_history_len = 100 + self.chat_history.extend(messages_to_move) if len(self.chat_history) > max_history_len: self.chat_history = self.chat_history[-max_history_len:] - # 更新历史记录字符串 (只使用最近一部分生成,例如20条) - history_slice_for_str = self.chat_history[-20:] + # 更新历史记录字符串 (仅使用最近一部分生成) + history_slice_for_str = self.chat_history[-20:] # 例如最近20条 try: self.chat_history_str = await build_readable_messages( history_slice_for_str, replace_bot_name=True, merge_messages=False, timestamp_mode="relative", - read_mark=0.0, # read_mark 可能需要根据逻辑调整 + read_mark=0.0, ) except Exception as e: logger.error(f"[私聊][{self.private_name}]构建聊天记录字符串时出错: {e}") - self.chat_history_str = "[构建聊天记录出错]" # 提供错误提示 + self.chat_history_str = "[构建聊天记录出错]" - # 清空未处理消息列表和计数 - # cleared_count = len(self.unprocessed_messages) - self.unprocessed_messages.clear() - self.new_messages_count = 0 - # self.has_unread_messages = False # 这个状态可以通过 new_messages_count 判断 + # 更新未处理消息列表和计数 + self.unprocessed_messages = remaining_messages + self.new_messages_count = len(self.unprocessed_messages) + self.chat_history_count = len(self.chat_history) - self.chat_history_count = len(self.chat_history) # 更新历史记录总数 - # logger.debug(f"[私聊][{self.private_name}]已处理 {cleared_count} 条消息,当前历史记录 {self.chat_history_count} 条。") + logger.info(f"[私聊][{self.private_name}]已清理 {cleared_count} 条消息,剩余未处理 {self.new_messages_count} 条,当前历史记录 {self.chat_history_count} 条。") - self.update_changed() # 状态改变 + self.update_changed() # 状态改变 \ No newline at end of file From 2df5124d8d468a84c3cb6658166127a25bdb070a Mon Sep 17 00:00:00 2001 From: Bakadax Date: Mon, 5 May 2025 22:01:29 +0800 Subject: [PATCH 67/67] modified: src/plugins/PFC/conversation.py --- src/plugins/PFC/conversation.py | 347 +++++++++++--------------------- 1 file changed, 113 insertions(+), 234 deletions(-) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 9f4bcfa0..5bb50706 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -4,24 +4,21 @@ import time import asyncio import datetime -# from .message_storage import MongoDBMessageStorage from src.plugins.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat - -# from ...config.config import global_config -from typing import Dict, Any, Optional +from typing import Dict, Any, Optional, Set # <-- 添加 Set 类型提示 from ..chat.message import Message from .pfc_types import ConversationState -from .pfc import ChatObserver, GoalAnalyzer # pfc.py 包含了 GoalAnalyzer,无需重复导入 +from .pfc import ChatObserver, GoalAnalyzer from .message_sender import DirectMessageSender from src.common.logger_manager import get_logger from .action_planner import ActionPlanner from .observation_info import ObservationInfo -from .conversation_info import ConversationInfo # 确保导入 ConversationInfo +from .conversation_info import ConversationInfo from .reply_generator import ReplyGenerator from ..chat.chat_stream import ChatStream from maim_message import UserInfo from src.plugins.chat.chat_stream import chat_manager -from .pfc_KnowledgeFetcher import KnowledgeFetcher # 注意:这里是 PFC_KnowledgeFetcher.py +from .pfc_KnowledgeFetcher import KnowledgeFetcher from .waiter import Waiter import traceback @@ -36,23 +33,16 @@ class Conversation: """对话类,负责管理单个对话的状态和行为""" def __init__(self, stream_id: str, private_name: str): - """初始化对话实例 - - Args: - stream_id: 聊天流ID - """ + """初始化对话实例""" self.stream_id = stream_id self.private_name = private_name self.state = ConversationState.INIT self.should_continue = False self.ignore_until_timestamp: Optional[float] = None - - # 回复相关 self.generated_reply = "" async def _initialize(self): - """初始化实例,注册所有组件""" - + """初始化实例,注册所有组件 (保持不变)""" try: self.action_planner = ActionPlanner(self.stream_id, self.private_name) self.goal_analyzer = GoalAnalyzer(self.stream_id, self.private_name) @@ -60,10 +50,7 @@ class Conversation: self.knowledge_fetcher = KnowledgeFetcher(self.private_name) self.waiter = Waiter(self.stream_id, self.private_name) self.direct_sender = DirectMessageSender(self.private_name) - - # 获取聊天流信息 self.chat_stream = chat_manager.get_stream(self.stream_id) - self.stop_action_planner = False except Exception as e: logger.error(f"[私聊][{self.private_name}]初始化对话实例:注册运行组件失败: {e}") @@ -71,14 +58,10 @@ class Conversation: raise try: - # 决策所需要的信息,包括自身自信和观察信息两部分 - # 注册观察器和观测信息 self.chat_observer = ChatObserver.get_instance(self.stream_id, self.private_name) self.chat_observer.start() self.observation_info = ObservationInfo(self.private_name) self.observation_info.bind_to_chat_observer(self.chat_observer) - # print(self.chat_observer.get_cached_messages(limit=) - self.conversation_info = ConversationInfo() except Exception as e: logger.error(f"[私聊][{self.private_name}]初始化对话实例:注册信息组件失败: {e}") @@ -86,10 +69,10 @@ class Conversation: raise try: logger.info(f"[私聊][{self.private_name}]为 {self.stream_id} 加载初始聊天记录...") - initial_messages = get_raw_msg_before_timestamp_with_chat( # + initial_messages = get_raw_msg_before_timestamp_with_chat( chat_id=self.stream_id, timestamp=time.time(), - limit=30, # 加载最近30条作为初始上下文,可以调整 + limit=30, ) chat_talking_prompt = await build_readable_messages( initial_messages, @@ -99,41 +82,31 @@ class Conversation: read_mark=0.0, ) if initial_messages: - # 将加载的消息填充到 ObservationInfo 的 chat_history self.observation_info.chat_history = initial_messages self.observation_info.chat_history_str = chat_talking_prompt + "\n" 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", "") - logger.info( f"[私聊][{self.private_name}]成功加载 {len(initial_messages)} 条初始聊天记录。最后一条消息时间: {self.observation_info.last_message_time}" ) - - # 让 ChatObserver 从加载的最后一条消息之后开始同步 - # **** 注意:这里的 last_message_time 设置可能需要 review **** - # 如果数据库消息时间戳可能不完全连续,直接设置 last_message_time 可能导致 observer 错过消息 - # 更稳妥的方式是让 observer 自己管理其内部的 last_message_time 或 last_message_id - # 暂时保留,但标记为潜在问题点。如果 observer 逻辑是可靠的,则此行 OK。 + # --- 注意: 下面两行保持不变,但其健壮性依赖于 ChatObserver 的实现 --- self.chat_observer.last_message_time = self.observation_info.last_message_time - self.chat_observer.last_message_read = last_msg # 更新 observer 的最后读取记录 + self.chat_observer.last_message_read = last_msg else: logger.info(f"[私聊][{self.private_name}]没有找到初始聊天记录。") except Exception as load_err: logger.error(f"[私聊][{self.private_name}]加载初始聊天记录时出错: {load_err}") - # 出错也要继续,只是没有历史记录而已 - # 组件准备完成,启动该论对话 + self.should_continue = True asyncio.create_task(self.start()) async def start(self): - """开始对话流程""" + """开始对话流程 (保持不变)""" try: logger.info(f"[私聊][{self.private_name}]对话系统启动中...") asyncio.create_task(self._plan_and_action_loop()) @@ -155,19 +128,15 @@ class Conversation: continue try: - # --- [修改点 1] 在规划前记录新消息状态 --- - # 记录规划开始时未处理消息的 ID 集合,用于后续判断哪些是新来的 + # --- [修改点 1] 在规划前记录未处理消息的 ID 集合 --- message_ids_before_planning = set() - initial_unprocessed_message_count = 0 # <-- 新增:记录规划前的未处理消息数 + initial_unprocessed_message_count = 0 if hasattr(self.observation_info, "unprocessed_messages"): message_ids_before_planning = {msg.get("message_id") for msg in self.observation_info.unprocessed_messages if msg.get("message_id")} - initial_unprocessed_message_count = len(self.observation_info.unprocessed_messages) # <-- 获取初始数量 + initial_unprocessed_message_count = len(self.observation_info.unprocessed_messages) logger.debug(f"[私聊][{self.private_name}]规划开始,当前未处理消息数: {initial_unprocessed_message_count}, IDs: {message_ids_before_planning}") else: - logger.warning( - f"[私聊][{self.private_name}]ObservationInfo missing 'unprocessed_messages' before planning." - ) - + logger.warning(f"[私聊][{self.private_name}]ObservationInfo missing 'unprocessed_messages' before planning.") # --- 调用 Action Planner (保持不变) --- action, reason = await self.action_planner.plan( @@ -179,69 +148,43 @@ class Conversation: current_unprocessed_message_count = 0 if hasattr(self.observation_info, "unprocessed_messages"): current_unprocessed_messages = self.observation_info.unprocessed_messages - current_unprocessed_message_count = len(current_unprocessed_messages) # <-- 获取当前数量 + current_unprocessed_message_count = len(current_unprocessed_messages) else: - logger.warning( - f"[私聊][{self.private_name}]ObservationInfo missing 'unprocessed_messages' after planning." - ) + logger.warning(f"[私聊][{self.private_name}]ObservationInfo missing 'unprocessed_messages' after planning.") # 计算规划期间实际新增的消息数量 new_messages_during_planning_count = 0 - new_message_ids_during_planning = set() for msg in current_unprocessed_messages: msg_id = msg.get("message_id") if msg_id and msg_id not in message_ids_before_planning: new_messages_during_planning_count += 1 - new_message_ids_during_planning.add(msg_id) logger.debug(f"[私聊][{self.private_name}]规划结束,当前未处理消息数: {current_unprocessed_message_count}, 规划期间新增: {new_messages_during_planning_count}") - # **核心逻辑:判断是否中断** - # 这里的 +2 是根据你的需求来的,代表允许的缓冲 - # 我们比较的是 *规划期间新增的消息数* 是否超过阈值 + # **核心逻辑:判断是否中断** (保持不变) if new_messages_during_planning_count > 2: logger.info( f"[私聊][{self.private_name}]规划期间新增消息数 ({new_messages_during_planning_count}) 超过阈值(2),取消本次行动 '{action}',重新规划" ) - # 中断时,重置上次回复状态,因为需要基于最新消息重新决策 self.conversation_info.last_successful_reply_action = None - # **重要**: 中断时不清空未处理消息,留给下一轮规划处理 - await asyncio.sleep(0.1) # 短暂暂停避免CPU空转 + await asyncio.sleep(0.1) continue # 跳过本轮后续处理,直接进入下一轮循环重新规划 - # --- [修改点 3] 准备执行动作,处理规划时已知的消息 --- - # 如果决定要回复 (direct_reply 或 send_new_message),并且规划开始时就有未处理消息 - # 这表示 LLM 规划时已经看到了这些消息 - # 我们需要在发送回复 *后* 清理掉这些规划时已知的消息 - # 注意:这里不再立即清理,清理逻辑移到 _handle_action 成功发送后 - messages_known_during_planning = [] - if action in ["direct_reply", "send_new_message"] and initial_unprocessed_message_count > 0: - messages_known_during_planning = [ - msg for msg_id in message_ids_before_planning - if (msg := next((m for m in self.observation_info.unprocessed_messages if m.get("message_id") == msg_id), None)) is not None - ] - logger.debug(f"[私聊][{self.private_name}]规划时已知 {len(messages_known_during_planning)} 条消息,将在回复成功后清理。") - - - # --- 执行动作 --- - # 将规划时已知需要清理的消息ID集合传递给 _handle_action - await self._handle_action(action, reason, self.observation_info, self.conversation_info, message_ids_before_planning) + # --- 执行动作 (移除 message_ids_before_planning 参数传递) --- + 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_item in self.conversation_info.goal_list: - current_goal = None # 初始化 current_goal + current_goal = None if isinstance(goal_item, dict): current_goal = goal_item.get("goal") - elif isinstance(goal_item, str): # 处理直接是字符串的情况 + elif isinstance(goal_item, str): current_goal = goal_item - - # 确保 current_goal 是字符串再比较 if isinstance(current_goal, str) and current_goal == "结束对话": goal_ended = True break - if goal_ended: self.should_continue = False logger.info(f"[私聊][{self.private_name}]检测到'结束对话'目标,停止循环。") @@ -249,57 +192,27 @@ class Conversation: except Exception as loop_err: logger.error(f"[私聊][{self.private_name}]PFC主循环出错: {loop_err}") logger.error(f"[私聊][{self.private_name}]{traceback.format_exc()}") - await asyncio.sleep(1) # 出错时暂停一下 + await asyncio.sleep(1) - # 循环间隔 if self.should_continue: - await asyncio.sleep(0.1) # 非阻塞的短暂停 + await asyncio.sleep(0.1) logger.info(f"[私聊][{self.private_name}]PFC 循环结束 for stream_id: {self.stream_id}") - # --- [修改点 4] 修改 _check_new_messages_after_planning --- - # 重命名并修改逻辑,用于在 *发送前* 检查是否有过多新消息(兜底检查) - def _check_interrupt_before_sending(self, message_ids_before_planning: set) -> bool: - """在发送回复前,最后检查一次是否有过多新消息导致需要中断""" - if not hasattr(self, "observation_info") or not hasattr(self.observation_info, "unprocessed_messages"): - logger.warning( - f"[私聊][{self.private_name}]ObservationInfo 未初始化或缺少 'unprocessed_messages' 属性,无法检查新消息。" - ) - return False - - current_unprocessed_messages = self.observation_info.unprocessed_messages - new_messages_count = 0 - for msg in current_unprocessed_messages: - msg_id = msg.get("message_id") - if msg_id and msg_id not in message_ids_before_planning: - new_messages_count += 1 - - # 使用与规划后检查相同的阈值 - if new_messages_count > 2: - logger.info( - f"[私聊][{self.private_name}]准备发送时发现新增消息数 ({new_messages_count}) 超过阈值(2),取消发送并重新规划" - ) - if hasattr(self, "conversation_info"): - self.conversation_info.last_successful_reply_action = None - else: - logger.warning( - f"[私聊][{self.private_name}]ConversationInfo 未初始化,无法重置 last_successful_reply_action。" - ) - return True # 需要中断 - return False # 不需要中断 - + # --- 移除 _check_interrupt_before_sending 方法 --- + # def _check_interrupt_before_sending(self, message_ids_before_planning: set) -> bool: + # ... (旧代码移除) def _convert_to_message(self, msg_dict: Dict[str, Any]) -> Message: """将消息字典转换为Message对象 (保持不变)""" try: - # 尝试从 msg_dict 直接获取 chat_stream,如果失败则从全局 chat_manager 获取 chat_info = msg_dict.get("chat_info") if chat_info and isinstance(chat_info, dict): chat_stream = ChatStream.from_dict(chat_info) - elif self.chat_stream: # 使用实例变量中的 chat_stream + elif self.chat_stream: chat_stream = self.chat_stream - else: # Fallback: 尝试从 manager 获取 (可能需要 stream_id) + else: chat_stream = chat_manager.get_stream(self.stream_id) if not chat_stream: raise ValueError(f"无法确定 ChatStream for stream_id {self.stream_id}") @@ -307,29 +220,26 @@ class Conversation: user_info = UserInfo.from_dict(msg_dict.get("user_info", {})) return Message( - message_id=msg_dict.get("message_id", f"gen_{time.time()}"), # 提供默认 ID - chat_stream=chat_stream, # 使用确定的 chat_stream - time=msg_dict.get("time", time.time()), # 提供默认时间 + message_id=msg_dict.get("message_id", f"gen_{time.time()}"), + chat_stream=chat_stream, + time=msg_dict.get("time", time.time()), user_info=user_info, processed_plain_text=msg_dict.get("processed_plain_text", ""), detailed_plain_text=msg_dict.get("detailed_plain_text", ""), ) except Exception as e: logger.warning(f"[私聊][{self.private_name}]转换消息时出错: {e}") - # 可以选择返回 None 或重新抛出异常,这里选择重新抛出以指示问题 raise ValueError(f"无法将字典转换为 Message 对象: {e}") from e - # --- [修改点 5] 修改 _handle_action 签名并调整内部逻辑 --- + # --- [修改点 3] 修改 _handle_action 签名并调整内部逻辑 (移除 message_ids_before_planning 参数) --- async def _handle_action( self, action: str, reason: str, observation_info: ObservationInfo, - conversation_info: ConversationInfo, - message_ids_before_planning: set # <-- 接收规划前的消息ID集合 + conversation_info: ConversationInfo ): """处理规划的行动""" - logger.debug(f"[私聊][{self.private_name}]执行行动: {action}, 原因: {reason}") # 记录action历史 (逻辑不变) @@ -346,11 +256,11 @@ class Conversation: action_index = len(conversation_info.done_action) - 1 action_successful = False - reply_sent = False # <-- 新增:标记是否成功发送了回复 + reply_sent = False # --- 根据不同的 action 执行 --- if action == "direct_reply" or action == "send_new_message": - # 合并 direct_reply 和 send_new_message 的大部分逻辑 + # 合并 reply 和 follow-up 的生成/检查逻辑 (保持不变) max_reply_attempts = 3 reply_attempt_count = 0 is_suitable = False @@ -364,24 +274,21 @@ class Conversation: logger.info(log_prefix) self.state = ConversationState.GENERATING - # 1. 生成回复 (传入 action_type) self.generated_reply = await self.reply_generator.generate( observation_info, conversation_info, action_type=action ) logger.info(f"{log_prefix} 生成内容: {self.generated_reply}") - # 2. 检查回复 (逻辑不变) self.state = ConversationState.CHECKING try: - current_goal_str = "" # 初始化 + current_goal_str = "" if hasattr(conversation_info, 'goal_list') and conversation_info.goal_list: - goal_item = conversation_info.goal_list[0] # 取第一个目标 + goal_item = conversation_info.goal_list[0] if isinstance(goal_item, dict): current_goal_str = goal_item.get('goal', '') elif isinstance(goal_item, str): current_goal_str = goal_item - # 确保 chat_history 和 chat_history_str 存在 chat_history_for_check = getattr(observation_info, 'chat_history', []) chat_history_str_for_check = getattr(observation_info, 'chat_history_str', '') @@ -396,42 +303,35 @@ class Conversation: f"{log_prefix} 检查结果: 合适={is_suitable}, 原因='{check_reason}', 需重新规划={need_replan}" ) - # 更新拒绝原因和内容 (仅在不合适或需要重规划时) if not is_suitable or need_replan: conversation_info.last_reply_rejection_reason = check_reason conversation_info.last_rejected_reply_content = self.generated_reply else: - # 检查通过,清空上次拒绝记录 conversation_info.last_reply_rejection_reason = None conversation_info.last_rejected_reply_content = None if is_suitable: final_reply_to_send = self.generated_reply - break # 检查通过,跳出循环 + break elif need_replan: logger.warning( f"{log_prefix} 检查建议重新规划,停止尝试。原因: {check_reason}" ) - break # 需要重新规划,跳出循环 + break except Exception as check_err: logger.error( f"{log_prefix} 调用 ReplyChecker 时出错: {check_err}" ) check_reason = f"第 {reply_attempt_count} 次检查过程出错: {check_err}" conversation_info.last_reply_rejection_reason = f"检查过程出错: {check_err}" - conversation_info.last_rejected_reply_content = self.generated_reply # 记录出错时尝试的内容 - break # 检查出错,跳出循环 + conversation_info.last_rejected_reply_content = self.generated_reply + break # --- 处理生成和检查的结果 --- if is_suitable: - # --- [修改点 6] 发送前最后检查是否需要中断 --- - if self._check_interrupt_before_sending(message_ids_before_planning): - logger.info(f"[私聊][{self.private_name}]生成回复后、发送前发现过多新消息,取消发送,重新规划") - conversation_info.done_action[action_index].update( - {"status": "recall", "final_reason": f"发送前发现过多新消息,取消发送: {final_reply_to_send}"} - ) - self.conversation_info.last_successful_reply_action = None # 重置状态 - return # 直接返回,主循环会重新规划 + # --- [修改点 4] 记录发送前时间戳 --- + timestamp_before_sending = time.time() + logger.debug(f"[私聊][{self.private_name}]准备发送回复,记录发送前时间戳: {timestamp_before_sending}") # 确认发送 self.generated_reply = final_reply_to_send @@ -439,46 +339,45 @@ class Conversation: if send_success: action_successful = True - reply_sent = True # 标记回复已发送 + reply_sent = True logger.info(f"[私聊][{self.private_name}]成功发送 '{action}' 回复.") - # 清空上次拒绝记录 (再次确保) conversation_info.last_reply_rejection_reason = None conversation_info.last_rejected_reply_content = None - # --- [修改点 7] 发送成功后,处理新消息并决定下一轮 prompt 类型 --- - # 获取发送后的最新未处理消息列表 - final_unprocessed_messages = getattr(observation_info, 'unprocessed_messages', []) - final_unprocessed_count = len(final_unprocessed_messages) + # --- [修改点 5] 基于时间戳处理消息和决定下一轮 prompt 类型 --- + current_unprocessed_messages = getattr(observation_info, 'unprocessed_messages', []) + message_ids_to_clear: Set[str] = set() # 使用 Set 类型 + new_messages_during_sending_count = 0 - # 计算在生成和发送期间新增的消息数 - new_messages_during_generation_count = 0 - for msg in final_unprocessed_messages: - msg_id = msg.get("message_id") - # 如果消息 ID 不在规划前的集合中,说明是新来的 - if msg_id and msg_id not in message_ids_before_planning: - new_messages_during_generation_count += 1 + for msg in current_unprocessed_messages: + msg_time = msg.get('time') + msg_id = msg.get('message_id') + if msg_id and msg_time: # 确保时间和 ID 存在 + if msg_time < timestamp_before_sending: + message_ids_to_clear.add(msg_id) + else: + # 时间戳大于等于发送前时间戳,视为新消息 + new_messages_during_sending_count += 1 - logger.debug(f"[私聊][{self.private_name}]回复发送后,当前未处理消息数: {final_unprocessed_count}, 其中生成/发送期间新增: {new_messages_during_generation_count}") + logger.debug(f"[私聊][{self.private_name}]回复发送后,检测到 {len(message_ids_to_clear)} 条发送前消息待清理,{new_messages_during_sending_count} 条发送期间/之后的新消息。") - # 根据生成期间是否有新消息,决定下次规划用哪个 prompt - if new_messages_during_generation_count > 0: - # 有 1 条或更多新消息在生成期间到达 - logger.info(f"[私聊][{self.private_name}]检测到 {new_messages_during_generation_count} 条在生成/发送期间到达的新消息,下一轮将使用首次回复逻辑处理。") + # 清理发送前到达的消息 + if message_ids_to_clear: + await observation_info.clear_processed_messages(message_ids_to_clear) + else: + logger.debug(f"[私聊][{self.private_name}]没有需要清理的发送前消息。") + + + # 根据发送期间是否有新消息,决定下次规划用哪个 prompt + if new_messages_during_sending_count > 0: + logger.info(f"[私聊][{self.private_name}]检测到 {new_messages_during_sending_count} 条在发送期间/之后到达的新消息,下一轮将使用首次回复逻辑处理。") self.conversation_info.last_successful_reply_action = None # 强制下一轮用 PROMPT_INITIAL_REPLY else: - # 没有新消息在生成期间到达 - logger.info(f"[私聊][{self.private_name}]生成/发送期间无新消息,下一轮将根据 '{action}' 使用追问逻辑。") + logger.info(f"[私聊][{self.private_name}]发送期间/之后无新消息,下一轮将根据 '{action}' 使用追问逻辑。") self.conversation_info.last_successful_reply_action = action # 保持状态,下一轮可能用 PROMPT_FOLLOW_UP - # --- [修改点 8] 清理规划时已知的消息 --- - # 只有在回复成功发送后,才清理掉那些在规划时就已经看到的消息 - if message_ids_before_planning: - await observation_info.clear_processed_messages(message_ids_before_planning) - - else: # 发送失败 logger.error(f"[私聊][{self.private_name}]发送 '{action}' 回复失败。") - # 发送失败,也认为动作未成功,重置状态 action_successful = False self.conversation_info.last_successful_reply_action = None conversation_info.done_action[action_index].update( @@ -486,25 +385,24 @@ class Conversation: ) elif need_replan: - # 检查后决定打回动作决策 + # 检查后打回动作决策 (保持不变) logger.warning( f"[私聊][{self.private_name}]'{action}' 回复检查后决定打回动作决策 (尝试 {reply_attempt_count} 次)。打回原因: {check_reason}" ) conversation_info.done_action[action_index].update( {"status": "recall", "final_reason": f"'{action}' 尝试{reply_attempt_count}次后打回: {check_reason}"} ) - self.conversation_info.last_successful_reply_action = None # 重置状态 + self.conversation_info.last_successful_reply_action = None - else: # 多次尝试后仍然不合适 (is_suitable is False and not need_replan) + else: # 多次尝试后仍然不合适 (保持不变) logger.warning( f"[私聊][{self.private_name}]经过 {reply_attempt_count} 次尝试,未能生成合适的 '{action}' 回复。最终原因: {check_reason}" ) conversation_info.done_action[action_index].update( {"status": "recall", "final_reason": f"'{action}' 尝试{reply_attempt_count}次后失败: {check_reason}"} ) - self.conversation_info.last_successful_reply_action = None # 重置状态 + self.conversation_info.last_successful_reply_action = None - # 如果是 send_new_message 失败,则执行 wait (保持原 fallback 逻辑) if action == "send_new_message": logger.info(f"[私聊][{self.private_name}]由于无法生成合适追问回复,执行 'wait' 操作...") self.state = ConversationState.WAITING @@ -512,20 +410,19 @@ class Conversation: wait_action_record = { "action": "wait", "plan_reason": "因 send_new_message 多次尝试失败而执行的后备等待", - "status": "done", # wait 本身算完成 + "status": "done", "time": datetime.datetime.now().strftime("%H:%M:%S"), "final_reason": None, } conversation_info.done_action.append(wait_action_record) - action_successful = True # fallback wait 成功 - # 注意: fallback wait 成功后,last_successful_reply_action 仍然是 None + action_successful = True + self.conversation_info.last_successful_reply_action = None - # --- 处理其他动作 (保持大部分不变,主要是确保状态重置) --- + # --- 处理其他动作 (保持不变,确保状态重置) --- elif action == "rethink_goal": self.state = ConversationState.RETHINKING try: if not hasattr(self, "goal_analyzer"): - logger.error(f"[私聊][{self.private_name}]GoalAnalyzer 未初始化,无法重新思考目标。") raise AttributeError("GoalAnalyzer not initialized") await self.goal_analyzer.analyze_goal(conversation_info, observation_info) action_successful = True @@ -534,9 +431,8 @@ class Conversation: conversation_info.done_action[action_index].update( {"status": "recall", "final_reason": f"重新思考目标失败: {rethink_err}"} ) - # 无论成功失败,非回复动作都重置 last_successful_reply_action self.conversation_info.last_successful_reply_action = None - conversation_info.last_reply_rejection_reason = None # 清除拒绝原因 + conversation_info.last_reply_rejection_reason = None conversation_info.last_rejected_reply_content = None @@ -545,7 +441,6 @@ class Conversation: logger.info(f"[私聊][{self.private_name}]倾听对方发言...") try: if not hasattr(self, "waiter"): - logger.error(f"[私聊][{self.private_name}]Waiter 未初始化,无法倾听。") raise AttributeError("Waiter not initialized") await self.waiter.wait_listening(conversation_info) action_successful = True @@ -554,7 +449,6 @@ class Conversation: conversation_info.done_action[action_index].update( {"status": "recall", "final_reason": f"倾听失败: {listen_err}"} ) - # 无论成功失败,非回复动作都重置 self.conversation_info.last_successful_reply_action = None conversation_info.last_reply_rejection_reason = None conversation_info.last_rejected_reply_content = None @@ -563,46 +457,47 @@ class Conversation: self.state = ConversationState.GENERATING logger.info(f"[私聊][{self.private_name}]执行行动: 生成并发送告别语...") try: - # 1. 生成告别语 self.generated_reply = await self.reply_generator.generate( observation_info, conversation_info, action_type="say_goodbye" ) logger.info(f"[私聊][{self.private_name}]生成的告别语: {self.generated_reply}") - # 2. 发送告别语 if self.generated_reply: - # --- [修改点 9] 告别前也检查中断 --- - if self._check_interrupt_before_sending(message_ids_before_planning): - logger.info(f"[私聊][{self.private_name}]发送告别语前发现过多新消息,取消发送,重新规划") - conversation_info.done_action[action_index].update( - {"status": "recall", "final_reason": "发送告别语前发现过多新消息"} - ) - self.should_continue = True # 不能结束,需要重规划 - self.conversation_info.last_successful_reply_action = None # 重置状态 - return - + # --- [修改点 6] 告别语发送前记录时间戳 --- + timestamp_before_sending_goodbye = time.time() send_success = await self._send_reply() if send_success: action_successful = True - reply_sent = True # 标记发送成功 + reply_sent = True logger.info(f"[私聊][{self.private_name}]告别语已发送。") - # 发送告别语成功后,通常意味着对话结束 - self.should_continue = False + + # --- [修改点 7] 告别语发送后也处理未读消息 --- + # (虽然通常之后就结束了,但以防万一) + current_unprocessed_messages_goodbye = getattr(observation_info, 'unprocessed_messages', []) + message_ids_to_clear_goodbye: Set[str] = set() + for msg in current_unprocessed_messages_goodbye: + msg_time = msg.get('time') + msg_id = msg.get('message_id') + if msg_id and msg_time and msg_time < timestamp_before_sending_goodbye: + message_ids_to_clear_goodbye.add(msg_id) + if message_ids_to_clear_goodbye: + await observation_info.clear_processed_messages(message_ids_to_clear_goodbye) + + self.should_continue = False # 正常结束 logger.info(f"[私聊][{self.private_name}]发送告别语流程结束,即将停止对话实例。") else: logger.warning(f"[私聊][{self.private_name}]发送告别语失败。") action_successful = False - # 发送失败不应结束对话,可能需要重试或做其他事 - self.should_continue = True + self.should_continue = True # 发送失败不能结束 conversation_info.done_action[action_index].update( {"status": "recall", "final_reason": "发送告别语失败"} ) - self.conversation_info.last_successful_reply_action = None # 重置状态 + self.conversation_info.last_successful_reply_action = None else: logger.warning(f"[私聊][{self.private_name}]未能生成告别语内容,无法发送。") action_successful = False - self.should_continue = True # 未能生成也不能结束 + self.should_continue = True conversation_info.done_action[action_index].update( {"status": "recall", "final_reason": "未能生成告别语内容"} ) @@ -612,7 +507,7 @@ class Conversation: logger.error(f"[私聊][{self.private_name}]生成或发送告别语时出错: {goodbye_err}") logger.error(f"[私聊][{self.private_name}]{traceback.format_exc()}") action_successful = False - self.should_continue = True # 出错也不能结束 + self.should_continue = True conversation_info.done_action[action_index].update( {"status": "recall", "final_reason": f"生成或发送告别语时出错: {goodbye_err}"} ) @@ -622,7 +517,6 @@ class Conversation: self.should_continue = False logger.info(f"[私聊][{self.private_name}]收到最终结束指令,停止对话...") action_successful = True - # 结束对话也重置状态 self.conversation_info.last_successful_reply_action = None conversation_info.last_reply_rejection_reason = None conversation_info.last_rejected_reply_content = None @@ -637,7 +531,6 @@ class Conversation: ) self.state = ConversationState.IGNORED action_successful = True - # 忽略也重置状态 self.conversation_info.last_successful_reply_action = None conversation_info.last_reply_rejection_reason = None conversation_info.last_rejected_reply_content = None @@ -648,7 +541,6 @@ class Conversation: logger.info(f"[私聊][{self.private_name}]等待更多信息...") try: if not hasattr(self, "waiter"): - logger.error(f"[私聊][{self.private_name}]Waiter 未初始化,无法等待。") raise AttributeError("Waiter not initialized") _timeout_occurred = await self.waiter.wait(self.conversation_info) action_successful = True @@ -657,13 +549,12 @@ class Conversation: conversation_info.done_action[action_index].update( {"status": "recall", "final_reason": f"等待失败: {wait_err}"} ) - # 无论成功失败,非回复动作都重置 self.conversation_info.last_successful_reply_action = None conversation_info.last_reply_rejection_reason = None conversation_info.last_rejected_reply_content = None - # --- 更新 Action History 状态 --- + # --- 更新 Action History 状态 (保持不变) --- if action_successful: conversation_info.done_action[action_index].update( { @@ -671,46 +562,35 @@ class Conversation: "time": datetime.datetime.now().strftime("%H:%M:%S"), } ) - # **注意**: last_successful_reply_action 的更新逻辑已经移到各自的动作处理中 logger.debug(f"[私聊][{self.private_name}]动作 '{action}' 标记为 'done'") else: - # 如果动作是 recall 状态,在各自的处理逻辑中已经更新了 done_action 的 final_reason logger.debug(f"[私聊][{self.private_name}]动作 '{action}' 标记为 'recall' 或失败") - # --- [修改点 10] _send_reply 返回布尔值表示成功与否 --- + async def _send_reply(self) -> bool: - """发送回复,并返回是否发送成功""" + """发送回复,并返回是否发送成功 (保持不变)""" if not self.generated_reply: logger.warning(f"[私聊][{self.private_name}]没有生成回复内容,无法发送。") - return False # 发送失败 + return False try: reply_content = self.generated_reply - - # 检查依赖项 if not hasattr(self, "direct_sender") or not self.direct_sender: logger.error(f"[私聊][{self.private_name}]DirectMessageSender 未初始化,无法发送回复。") - return False # 发送失败 + return False if not self.chat_stream: logger.error(f"[私聊][{self.private_name}]ChatStream 未初始化,无法发送回复。") - return False # 发送失败 + return False - # 发送消息 await self.direct_sender.send_message(chat_stream=self.chat_stream, content=reply_content) - - # 发送成功后,可以考虑触发 observer 更新,但需谨慎避免竞争条件或重复处理 - # 暂时注释掉,依赖 observer 的自然更新周期 - # self.chat_observer.trigger_update() - # await self.chat_observer.wait_for_update() - - self.state = ConversationState.ANALYZING # 更新状态 (例如,可以改为 IDLE 或 WAITING) - return True # 发送成功 + self.state = ConversationState.ANALYZING + return True except Exception as e: logger.error(f"[私聊][{self.private_name}]发送消息时失败: {str(e)}") logger.error(f"[私聊][{self.private_name}]{traceback.format_exc()}") - self.state = ConversationState.ANALYZING # 或者设置为 ERROR 状态? - return False # 发送失败 + self.state = ConversationState.ANALYZING + return False async def _send_timeout_message(self): @@ -719,7 +599,6 @@ class Conversation: messages = self.chat_observer.get_cached_messages(limit=1) if not messages: return - latest_message = self._convert_to_message(messages[0]) await self.direct_sender.send_message( chat_stream=self.chat_stream, content="TODO:超时消息", reply_to_message=latest_message