feat:对HFC进行巨大重构,采用新架构

This commit is contained in:
SengokuCola
2025-05-12 11:49:14 +08:00
parent e5a756f156
commit 05f0aaa6d7
33 changed files with 2221 additions and 1738 deletions

View File

@@ -100,6 +100,7 @@ class MessageRecv(Message):
Args:
message_dict: MessageCQ序列化后的字典
"""
# print(f"message_dict: {message_dict}")
self.message_info = BaseMessageInfo.from_dict(message_dict.get("message_info", {}))
self.message_segment = Seg.from_dict(message_dict.get("message_segment", {}))

View File

@@ -212,7 +212,7 @@ 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}")
# 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
)
@@ -236,7 +236,7 @@ class MessageManager:
await message.process() # 预处理消息内容
logger.debug(f"{message}")
# logger.debug(f"{message}")
# 使用全局 message_sender 实例
await send_message(message)

View File

@@ -117,7 +117,7 @@ class ImageManager:
cached_description = self._get_description_from_db(image_hash, "emoji")
if cached_description:
# logger.debug(f"缓存表情包描述: {cached_description}")
return f"[表达了{cached_description}]"
return f"[表情包,含义看起来是{cached_description}]"
# 调用AI获取描述
if image_format == "gif" or image_format == "GIF":
@@ -131,7 +131,7 @@ class ImageManager:
cached_description = self._get_description_from_db(image_hash, "emoji")
if cached_description:
logger.warning(f"虽然生成了描述,但是找到缓存表情包描述: {cached_description}")
return f"[表达了{cached_description}]"
return f"[表情包,含义看起来是{cached_description}]"
# 根据配置决定是否保存图片
if global_config.save_emoji:

View File

@@ -1,48 +1,47 @@
import os
import time
import json
from typing import List, Dict, Any, Optional, Tuple
from typing import List, Dict, Any, Tuple
from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo
from src.common.logger_manager import get_logger
logger = get_logger("cycle_analyzer")
class CycleAnalyzer:
"""循环信息分析类提供查询和分析CycleInfo的工具"""
def __init__(self, base_dir: str = "log_debug"):
"""
初始化分析器
参数:
base_dir: 存储CycleInfo的基础目录默认为log_debug
"""
self.base_dir = base_dir
def list_streams(self) -> List[str]:
"""
获取所有聊天流ID列表
返回:
List[str]: 聊天流ID列表
"""
try:
if not os.path.exists(self.base_dir):
return []
return [d for d in os.listdir(self.base_dir)
if os.path.isdir(os.path.join(self.base_dir, d))]
return [d for d in os.listdir(self.base_dir) if os.path.isdir(os.path.join(self.base_dir, d))]
except Exception as e:
logger.error(f"获取聊天流列表时出错: {e}")
return []
def get_stream_cycle_count(self, stream_id: str) -> int:
"""
获取指定聊天流的循环数量
参数:
stream_id: 聊天流ID
返回:
int: 循环数量
"""
@@ -52,16 +51,16 @@ class CycleAnalyzer:
except Exception as e:
logger.error(f"获取聊天流循环数量时出错: {e}")
return 0
def get_stream_cycles(self, stream_id: str, start: int = 0, limit: int = -1) -> List[str]:
"""
获取指定聊天流的循环文件列表
参数:
stream_id: 聊天流ID
start: 起始索引默认为0
limit: 返回的最大数量,默认为-1全部
返回:
List[str]: 循环文件路径列表
"""
@@ -70,38 +69,38 @@ class CycleAnalyzer:
if limit < 0:
return files[start:]
else:
return files[start:start+limit]
return files[start : start + limit]
except Exception as e:
logger.error(f"获取聊天流循环文件列表时出错: {e}")
return []
def get_cycle_content(self, filepath: str) -> str:
"""
获取循环文件的内容
参数:
filepath: 文件路径
返回:
str: 文件内容
"""
try:
if not os.path.exists(filepath):
return f"文件不存在: {filepath}"
with open(filepath, 'r', encoding='utf-8') as f:
with open(filepath, "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
logger.error(f"读取循环文件内容时出错: {e}")
return f"读取文件出错: {e}"
def analyze_stream_cycles(self, stream_id: str) -> Dict[str, Any]:
"""
分析指定聊天流的所有循环,生成统计信息
参数:
stream_id: 聊天流ID
返回:
Dict[str, Any]: 统计信息
"""
@@ -109,75 +108,75 @@ class CycleAnalyzer:
files = CycleInfo.list_cycles(stream_id, self.base_dir)
if not files:
return {"error": "没有找到循环记录"}
total_cycles = len(files)
action_counts = {"text_reply": 0, "emoji_reply": 0, "no_reply": 0, "unknown": 0}
total_duration = 0
tool_usage = {}
for filepath in files:
with open(filepath, 'r', encoding='utf-8') as f:
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
# 解析动作类型
for line in content.split('\n'):
for line in content.split("\n"):
if line.startswith("动作:"):
action = line[3:].strip()
action_counts[action] = action_counts.get(action, 0) + 1
# 解析耗时
elif line.startswith("耗时:"):
try:
duration = float(line[3:].strip().split('')[0])
duration = float(line[3:].strip().split("")[0])
total_duration += duration
except:
pass
# 解析工具使用
elif line.startswith("使用的工具:"):
tools = line[6:].strip().split(', ')
tools = line[6:].strip().split(", ")
for tool in tools:
tool_usage[tool] = tool_usage.get(tool, 0) + 1
avg_duration = total_duration / total_cycles if total_cycles > 0 else 0
return {
"总循环数": total_cycles,
"动作统计": action_counts,
"平均耗时": f"{avg_duration:.2f}",
"总耗时": f"{total_duration:.2f}",
"工具使用次数": tool_usage
"工具使用次数": tool_usage,
}
except Exception as e:
logger.error(f"分析聊天流循环时出错: {e}")
return {"error": f"分析出错: {e}"}
def get_latest_cycles(self, count: int = 10) -> List[Tuple[str, str]]:
"""
获取所有聊天流中最新的几个循环
参数:
count: 获取的数量默认为10
返回:
List[Tuple[str, str]]: 聊天流ID和文件路径的元组列表
"""
try:
all_cycles = []
streams = self.list_streams()
for stream_id in streams:
files = CycleInfo.list_cycles(stream_id, self.base_dir)
for filepath in files:
try:
# 从文件名中提取时间戳
filename = os.path.basename(filepath)
timestamp_str = filename.split('_', 2)[2].split('.')[0]
timestamp_str = filename.split("_", 2)[2].split(".")[0]
timestamp = time.mktime(time.strptime(timestamp_str, "%Y%m%d_%H%M%S"))
all_cycles.append((timestamp, stream_id, filepath))
except:
continue
# 按时间戳排序取最新的count个
all_cycles.sort(reverse=True)
return [(item[1], item[2]) for item in all_cycles[:count]]
@@ -189,11 +188,11 @@ class CycleAnalyzer:
# 使用示例
if __name__ == "__main__":
analyzer = CycleAnalyzer()
# 列出所有聊天流
streams = analyzer.list_streams()
print(f"找到 {len(streams)} 个聊天流: {streams}")
# 分析第一个聊天流的循环
if streams:
stream_id = streams[0]
@@ -201,15 +200,15 @@ if __name__ == "__main__":
print(f"\n聊天流 {stream_id} 的统计信息:")
for key, value in stats.items():
print(f" {key}: {value}")
# 获取最新的循环
cycles = analyzer.get_stream_cycles(stream_id, limit=1)
if cycles:
print(f"\n最新循环内容:")
print("\n最新循环内容:")
print(analyzer.get_cycle_content(cycles[0]))
# 获取所有聊天流中最新的3个循环
latest_cycles = analyzer.get_latest_cycles(3)
print(f"\n所有聊天流中最新的 {len(latest_cycles)} 个循环:")
for stream_id, filepath in latest_cycles:
print(f" 聊天流 {stream_id}: {os.path.basename(filepath)}")
print(f" 聊天流 {stream_id}: {os.path.basename(filepath)}")

View File

@@ -1,119 +1,123 @@
import os
import sys
import argparse
from typing import List, Dict, Any
from src.plugins.heartFC_chat.cycle_analyzer import CycleAnalyzer
def print_section(title: str, width: int = 80):
"""打印分隔线和标题"""
print("\n" + "=" * width)
print(f" {title} ".center(width, "="))
print("=" * width)
def list_streams_cmd(analyzer: CycleAnalyzer, args: argparse.Namespace):
"""列出所有聊天流"""
print_section("所有聊天流")
streams = analyzer.list_streams()
if not streams:
print("没有找到任何聊天流记录。")
return
for i, stream_id in enumerate(streams):
count = analyzer.get_stream_cycle_count(stream_id)
print(f"[{i+1}] {stream_id} - {count} 个循环")
print(f"[{i + 1}] {stream_id} - {count} 个循环")
def analyze_stream_cmd(analyzer: CycleAnalyzer, args: argparse.Namespace):
"""分析指定聊天流的循环信息"""
stream_id = args.stream_id
print_section(f"聊天流 {stream_id} 分析")
stats = analyzer.analyze_stream_cycles(stream_id)
if "error" in stats:
print(f"错误: {stats['error']}")
return
print("基本统计:")
print(f" 总循环数: {stats['总循环数']}")
print(f" 总耗时: {stats['总耗时']}")
print(f" 平均耗时: {stats['平均耗时']}")
print("\n动作统计:")
for action, count in stats['动作统计'].items():
for action, count in stats["动作统计"].items():
if count > 0:
percent = (count / stats['总循环数']) * 100
percent = (count / stats["总循环数"]) * 100
print(f" {action}: {count} ({percent:.1f}%)")
if stats.get('工具使用次数'):
if stats.get("工具使用次数"):
print("\n工具使用次数:")
for tool, count in stats['工具使用次数'].items():
for tool, count in stats["工具使用次数"].items():
print(f" {tool}: {count}")
def list_cycles_cmd(analyzer: CycleAnalyzer, args: argparse.Namespace):
"""列出指定聊天流的循环"""
stream_id = args.stream_id
limit = args.limit if args.limit > 0 else -1
print_section(f"聊天流 {stream_id} 的循环列表")
cycles = analyzer.get_stream_cycles(stream_id)
if not cycles:
print("没有找到任何循环记录。")
return
if limit > 0:
cycles = cycles[-limit:] # 取最新的limit个
print(f"显示最新的 {limit} 个循环 (共 {len(cycles)} 个):")
else:
print(f"共找到 {len(cycles)} 个循环:")
for i, filepath in enumerate(cycles):
filename = os.path.basename(filepath)
cycle_id = filename.split('_')[1]
timestamp = filename.split('_', 2)[2].split('.')[0]
print(f"[{i+1}] 循环ID: {cycle_id}, 时间: {timestamp}, 文件: {filename}")
cycle_id = filename.split("_")[1]
timestamp = filename.split("_", 2)[2].split(".")[0]
print(f"[{i + 1}] 循环ID: {cycle_id}, 时间: {timestamp}, 文件: {filename}")
def view_cycle_cmd(analyzer: CycleAnalyzer, args: argparse.Namespace):
"""查看指定循环的详细信息"""
stream_id = args.stream_id
cycle_index = args.cycle_index - 1 # 转换为0-based索引
cycles = analyzer.get_stream_cycles(stream_id)
if not cycles:
print(f"错误: 聊天流 {stream_id} 没有找到任何循环记录。")
return
if cycle_index < 0 or cycle_index >= len(cycles):
print(f"错误: 循环索引 {args.cycle_index} 超出范围 (1-{len(cycles)})。")
return
filepath = cycles[cycle_index]
filename = os.path.basename(filepath)
print_section(f"循环详情: {filename}")
content = analyzer.get_cycle_content(filepath)
print(content)
def latest_cycles_cmd(analyzer: CycleAnalyzer, args: argparse.Namespace):
"""查看所有聊天流中最新的几个循环"""
count = args.count if args.count > 0 else 10
print_section(f"最新的 {count} 个循环")
latest_cycles = analyzer.get_latest_cycles(count)
if not latest_cycles:
print("没有找到任何循环记录。")
return
for i, (stream_id, filepath) in enumerate(latest_cycles):
filename = os.path.basename(filepath)
cycle_id = filename.split('_')[1]
timestamp = filename.split('_', 2)[2].split('.')[0]
print(f"[{i+1}] 聊天流: {stream_id}, 循环ID: {cycle_id}, 时间: {timestamp}")
cycle_id = filename.split("_")[1]
timestamp = filename.split("_", 2)[2].split(".")[0]
print(f"[{i + 1}] 聊天流: {stream_id}, 循环ID: {cycle_id}, 时间: {timestamp}")
# 可以选择性添加提取基本信息的功能
with open(filepath, 'r', encoding='utf-8') as f:
with open(filepath, "r", encoding="utf-8") as f:
for line in f:
if line.startswith("动作:"):
action = line.strip()
@@ -121,35 +125,36 @@ def latest_cycles_cmd(analyzer: CycleAnalyzer, args: argparse.Namespace):
break
print()
def main():
parser = argparse.ArgumentParser(description="HeartFC循环信息查看工具")
subparsers = parser.add_subparsers(dest="command", help="子命令")
# 列出所有聊天流
list_streams_parser = subparsers.add_parser("list-streams", help="列出所有聊天流")
# 分析聊天流
analyze_parser = subparsers.add_parser("analyze", help="分析指定聊天流的循环信息")
analyze_parser.add_argument("stream_id", help="聊天流ID")
# 列出聊天流的循环
list_cycles_parser = subparsers.add_parser("list-cycles", help="列出指定聊天流的循环")
list_cycles_parser.add_argument("stream_id", help="聊天流ID")
list_cycles_parser.add_argument("-l", "--limit", type=int, default=-1, help="显示最新的N个循环")
# 查看指定循环
view_parser = subparsers.add_parser("view", help="查看指定循环的详细信息")
view_parser.add_argument("stream_id", help="聊天流ID")
view_parser.add_argument("cycle_index", type=int, help="循环索引从1开始")
# 查看最新循环
latest_parser = subparsers.add_parser("latest", help="查看所有聊天流中最新的几个循环")
latest_parser.add_argument("-c", "--count", type=int, default=10, help="显示的数量")
args = parser.parse_args()
analyzer = CycleAnalyzer()
if args.command == "list-streams":
list_streams_cmd(analyzer, args)
elif args.command == "analyze":
@@ -163,5 +168,6 @@ def main():
else:
parser.print_help()
if __name__ == "__main__":
main()
main()

View File

@@ -0,0 +1,319 @@
import time
import traceback
from typing import List, Optional, Dict, Any
from src.plugins.chat.message import MessageRecv, MessageThinking, MessageSending
from src.plugins.chat.message import Seg # Local import needed after move
from src.plugins.chat.message import UserInfo
from src.plugins.chat.chat_stream import chat_manager
from src.common.logger_manager import get_logger
from src.plugins.models.utils_model import LLMRequest
from src.config.config import global_config
from src.plugins.chat.utils_image import image_path_to_base64 # Local import needed after move
from src.plugins.utils.timer_calculator import Timer # <--- Import Timer
from src.plugins.emoji_system.emoji_manager import emoji_manager
from src.plugins.heartFC_chat.heartflow_prompt_builder import prompt_builder
from src.plugins.heartFC_chat.heartFC_sender import HeartFCSender
from src.plugins.chat.utils import process_llm_response
from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
from src.plugins.moods.moods import MoodManager
from src.heart_flow.utils_chat import get_chat_type_and_target_info
from src.plugins.chat.chat_stream import ChatStream
logger = get_logger("expressor")
class DefaultExpressor:
def __init__(self, chat_id: str):
self.log_prefix = "expressor"
self.express_model = LLMRequest(
model=global_config.llm_normal,
temperature=global_config.llm_normal["temp"],
max_tokens=256,
request_type="response_heartflow",
)
self.heart_fc_sender = HeartFCSender()
self.chat_id = chat_id
self.chat_stream: Optional[ChatStream] = None
self.is_group_chat = True
self.chat_target_info = None
async def initialize(self):
self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.chat_id)
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,
)
logger.debug(f"创建思考消息:{anchor_message}")
logger.debug(f"创建思考消息chat{chat}")
logger.debug(f"创建思考消息bot_user_info{bot_user_info}")
logger.debug(f"创建思考消息messageinfo{messageinfo}")
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,
)
logger.debug(f"创建思考消息thinking_message{thinking_message}")
# Access MessageManager directly (using heart_fc_sender)
await self.heart_fc_sender.register_thinking(thinking_message)
return thinking_id
async def deal_reply(
self,
cycle_timers: dict,
action_data: Dict[str, Any],
reasoning: str,
anchor_message: MessageRecv,
) -> tuple[bool, str]:
# 创建思考消息
thinking_id = await self._create_thinking_message(anchor_message)
if not thinking_id:
raise Exception("无法创建思考消息")
try:
has_sent_something = False
# 处理文本部分
text_part = action_data.get("text", [])
if text_part:
with Timer("生成回复", cycle_timers):
# 可以保留原有的文本处理逻辑或进行适当调整
reply = await self.express(
in_mind_reply=text_part,
anchor_message=anchor_message,
thinking_id=thinking_id,
reason=reasoning,
)
if reply:
with Timer("发送文本消息", cycle_timers):
await self._send_response_messages(
anchor_message=anchor_message,
thinking_id=thinking_id,
response_set=reply,
)
has_sent_something = True
else:
logger.warning(f"{self.log_prefix} 文本回复生成失败")
# 处理表情部分
emoji_keyword = action_data.get("emojis", [])
if emoji_keyword:
await self._handle_emoji(anchor_message, [], emoji_keyword)
has_sent_something = True
if not has_sent_something:
logger.warning(f"{self.log_prefix} 回复动作未包含任何有效内容")
return has_sent_something, thinking_id
except Exception as e:
logger.error(f"回复失败: {e}")
return False, thinking_id
# --- 回复器 (Replier) 的定义 --- #
async def express(
self,
in_mind_reply: str,
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.express_model.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
in_mind_reply=in_mind_reply,
reason=reason,
current_mind_info="",
structured_info="",
sender_name=sender_name_for_prompt, # Pass determined name
)
# 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.express_model.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
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}")
traceback.print_exc()
return None
# --- 发送器 (Sender) --- #
async def _send_response_messages(
self, anchor_message: Optional[MessageRecv], response_set: List[str], thinking_id: str
) -> Optional[MessageSending]:
"""发送回复消息 (尝试锚定到 anchor_message),使用 HeartFCSender"""
if not anchor_message or not anchor_message.chat_stream:
logger.error(f"{self.log_prefix} 无法发送回复,缺少有效的锚点消息或聊天流。")
return None
chat = self.chat_stream
chat_id = self.chat_id
stream_name = chat_manager.get_stream_name(chat_id) or chat_id # 获取流名称用于日志
# 检查思考过程是否仍在进行,并获取开始时间
thinking_start_time = await self.heart_fc_sender.get_thinking_start_time(chat_id, thinking_id)
if thinking_start_time is None:
logger.warning(f"[{stream_name}] {thinking_id} 思考过程未找到或已结束,无法发送回复。")
return None
mark_head = False
first_bot_msg: Optional[MessageSending] = None
reply_message_ids = [] # 记录实际发送的消息ID
bot_user_info = UserInfo(
user_id=global_config.BOT_QQ,
user_nickname=global_config.BOT_NICKNAME,
platform=chat.platform,
)
for i, msg_text in enumerate(response_set):
# 为每个消息片段生成唯一ID
part_message_id = f"{thinking_id}_{i}"
message_segment = Seg(type="text", data=msg_text)
bot_message = MessageSending(
message_id=part_message_id, # 使用片段的唯一ID
chat_stream=chat,
bot_user_info=bot_user_info,
sender_info=anchor_message.message_info.user_info,
message_segment=message_segment,
reply=anchor_message, # 回复原始锚点
is_head=not mark_head,
is_emoji=False,
thinking_start_time=thinking_start_time, # 传递原始思考开始时间
)
try:
if not mark_head:
mark_head = True
first_bot_msg = bot_message # 保存第一个成功发送的消息对象
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, typing=True)
reply_message_ids.append(part_message_id) # 记录我们生成的ID
except Exception as e:
logger.error(
f"{self.log_prefix}[Sender-{thinking_id}] 发送回复片段 {i} ({part_message_id}) 时失败: {e}"
)
# 这里可以选择是继续发送下一个片段还是中止
# 在尝试发送完所有片段后,完成原始的 thinking_id 状态
try:
await self.heart_fc_sender.complete_thinking(chat_id, thinking_id)
except Exception as e:
logger.error(f"{self.log_prefix}[Sender-{thinking_id}] 完成思考状态 {thinking_id} 时出错: {e}")
return first_bot_msg # 返回第一个成功发送的消息对象
async def _handle_emoji(self, anchor_message: Optional[MessageRecv], response_set: List[str], send_emoji: str = ""):
"""处理表情包 (尝试锚定到 anchor_message),使用 HeartFCSender"""
if not anchor_message or not anchor_message.chat_stream:
logger.error(f"{self.log_prefix} 无法处理表情包,缺少有效的锚点消息或聊天流。")
return
chat = anchor_message.chat_stream
emoji_raw = await emoji_manager.get_emoji_for_text(send_emoji)
if emoji_raw:
emoji_path, description = emoji_raw
emoji_cq = image_path_to_base64(emoji_path)
thinking_time_point = round(time.time(), 2) # 用于唯一ID
message_segment = Seg(type="emoji", data=emoji_cq)
bot_user_info = UserInfo(
user_id=global_config.BOT_QQ,
user_nickname=global_config.BOT_NICKNAME,
platform=anchor_message.message_info.platform,
)
bot_message = MessageSending(
message_id="me" + str(thinking_time_point), # 表情消息的唯一ID
chat_stream=chat,
bot_user_info=bot_user_info,
sender_info=anchor_message.message_info.user_info,
message_segment=message_segment,
reply=anchor_message, # 回复原始锚点
is_head=False, # 表情通常不是头部消息
is_emoji=True,
# 不需要 thinking_start_time
)
try:
await self.heart_fc_sender.send_and_store(bot_message)
except Exception as e:
logger.error(f"{self.log_prefix} 发送表情包 {bot_message.message_info.message_id} 时失败: {e}")

View File

@@ -4,7 +4,7 @@ import json
from typing import List, Optional, Dict, Any
class CycleInfo:
class CycleDetail:
"""循环信息记录类"""
def __init__(self, cycle_id: int):
@@ -27,21 +27,21 @@ class CycleInfo:
"sub_mind_thinking": "", # 子思维思考内容
"in_mind_reply": [], # 子思维思考内容
}
# 添加SubMind相关信息
self.submind_info: Dict[str, Any] = {
"prompt": "", # SubMind输入的prompt
"structured_info": "", # 结构化信息
"result": "", # SubMind的思考结果
}
# 添加ToolUse相关信息
self.tooluse_info: Dict[str, Any] = {
"prompt": "", # 工具使用的prompt
"tools_used": [], # 使用了哪些工具
"tool_results": [], # 工具获得的信息
}
# 添加Planner相关信息
self.planner_info: Dict[str, Any] = {
"prompt": "", # 规划器的prompt
@@ -70,9 +70,12 @@ class CycleInfo:
"""完成循环,记录结束时间"""
self.end_time = time.time()
def set_action_info(self, action_type: str, reasoning: str, action_taken: bool):
def set_action_info(
self, action_type: str, reasoning: str, action_taken: bool, action_data: Optional[Dict[str, Any]] = None
):
"""设置动作信息"""
self.action_type = action_type
self.action_data = action_data
self.reasoning = reasoning
self.action_taken = action_taken
@@ -99,7 +102,7 @@ class CycleInfo:
self.response_info["reply_message_ids"] = reply_message_ids
if sub_mind_thinking is not None:
self.response_info["sub_mind_thinking"] = sub_mind_thinking
def set_submind_info(
self,
prompt: Optional[str] = None,
@@ -113,7 +116,7 @@ class CycleInfo:
self.submind_info["structured_info"] = structured_info
if result is not None:
self.submind_info["result"] = result
def set_tooluse_info(
self,
prompt: Optional[str] = None,
@@ -127,7 +130,7 @@ class CycleInfo:
self.tooluse_info["tools_used"] = tools_used
if tool_results is not None:
self.tooluse_info["tool_results"] = tool_results
def set_planner_info(
self,
prompt: Optional[str] = None,
@@ -141,17 +144,17 @@ class CycleInfo:
self.planner_info["response"] = response
if parsed_result is not None:
self.planner_info["parsed_result"] = parsed_result
@staticmethod
def save_to_file(cycle_info: 'CycleInfo', stream_id: str, base_dir: str = "log_debug") -> str:
def save_to_file(cycle_info: "CycleDetail", stream_id: str, base_dir: str = "log_debug") -> str:
"""
将CycleInfo保存到文件
参数:
cycle_info: CycleInfo对象
stream_id: 聊天流ID
base_dir: 基础目录默认为log_debug
返回:
str: 保存的文件路径
"""
@@ -159,17 +162,17 @@ class CycleInfo:
# 创建目录结构
stream_dir = os.path.join(base_dir, stream_id)
os.makedirs(stream_dir, exist_ok=True)
# 生成文件名和路径
timestamp = time.strftime("%Y%m%d_%H%M%S", time.localtime(cycle_info.start_time))
filename = f"cycle_{cycle_info.cycle_id}_{timestamp}.txt"
filepath = os.path.join(stream_dir, filename)
# 将CycleInfo转换为JSON格式
cycle_data = cycle_info.to_dict()
# 格式化输出成易读的格式
with open(filepath, 'w', encoding='utf-8') as f:
with open(filepath, "w", encoding="utf-8") as f:
# 写入基本信息
f.write(f"循环ID: {cycle_info.cycle_id}\n")
f.write(f"开始时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(cycle_info.start_time))}\n")
@@ -182,7 +185,7 @@ class CycleInfo:
f.write(f"执行状态: {'已执行' if cycle_info.action_taken else '未执行'}\n")
f.write(f"思考ID: {cycle_info.thinking_id}\n")
f.write(f"是否为重新规划: {'' if cycle_info.replanned else ''}\n\n")
# 写入计时器信息
if cycle_info.timers:
f.write("== 计时器信息 ==\n")
@@ -190,42 +193,42 @@ class CycleInfo:
formatted_time = f"{elapsed * 1000:.2f}毫秒" if elapsed < 1 else f"{elapsed:.2f}"
f.write(f"{name}: {formatted_time}\n")
f.write("\n")
# 写入响应信息
f.write("== 响应信息 ==\n")
f.write(f"锚点消息ID: {cycle_info.response_info['anchor_message_id']}\n")
if cycle_info.response_info['response_text']:
if cycle_info.response_info["response_text"]:
f.write("回复文本:\n")
for i, text in enumerate(cycle_info.response_info['response_text']):
f.write(f" [{i+1}] {text}\n")
if cycle_info.response_info['emoji_info']:
for i, text in enumerate(cycle_info.response_info["response_text"]):
f.write(f" [{i + 1}] {text}\n")
if cycle_info.response_info["emoji_info"]:
f.write(f"表情信息: {cycle_info.response_info['emoji_info']}\n")
if cycle_info.response_info['reply_message_ids']:
if cycle_info.response_info["reply_message_ids"]:
f.write(f"回复消息ID: {', '.join(cycle_info.response_info['reply_message_ids'])}\n")
f.write("\n")
# 写入SubMind信息
f.write("== SubMind信息 ==\n")
f.write(f"结构化信息:\n{cycle_info.submind_info['structured_info']}\n\n")
f.write(f"思考结果:\n{cycle_info.submind_info['result']}\n\n")
f.write("SubMind Prompt:\n")
f.write(f"{cycle_info.submind_info['prompt']}\n\n")
# 写入ToolUse信息
f.write("== 工具使用信息 ==\n")
if cycle_info.tooluse_info['tools_used']:
if cycle_info.tooluse_info["tools_used"]:
f.write(f"使用的工具: {', '.join(cycle_info.tooluse_info['tools_used'])}\n")
else:
f.write("未使用工具\n")
if cycle_info.tooluse_info['tool_results']:
if cycle_info.tooluse_info["tool_results"]:
f.write("工具结果:\n")
for i, result in enumerate(cycle_info.tooluse_info['tool_results']):
f.write(f" [{i+1}] 类型: {result.get('type', '未知')}, 内容: {result.get('content', '')}\n")
for i, result in enumerate(cycle_info.tooluse_info["tool_results"]):
f.write(f" [{i + 1}] 类型: {result.get('type', '未知')}, 内容: {result.get('content', '')}\n")
f.write("\n")
f.write("工具执行 Prompt:\n")
f.write(f"{cycle_info.tooluse_info['prompt']}\n\n")
# 写入Planner信息
f.write("== Planner信息 ==\n")
f.write("Planner Prompt:\n")
@@ -234,7 +237,7 @@ class CycleInfo:
f.write(f"{cycle_info.planner_info['response']}\n\n")
f.write("解析结果:\n")
f.write(f"{json.dumps(cycle_info.planner_info['parsed_result'], ensure_ascii=False, indent=2)}\n")
return filepath
except Exception as e:
print(f"保存CycleInfo到文件时出错: {e}")
@@ -244,10 +247,10 @@ class CycleInfo:
def load_from_file(filepath: str) -> Optional[Dict[str, Any]]:
"""
从文件加载CycleInfo信息只加载JSON格式的数据不解析文本格式
参数:
filepath: 文件路径
返回:
Optional[Dict[str, Any]]: 加载的CycleInfo数据失败则返回None
"""
@@ -255,39 +258,39 @@ class CycleInfo:
if not os.path.exists(filepath):
print(f"文件不存在: {filepath}")
return None
# 尝试从文件末尾读取JSON数据
with open(filepath, 'r', encoding='utf-8') as f:
with open(filepath, "r", encoding="utf-8") as f:
lines = f.readlines()
# 查找"解析结果:"后的JSON数据
for i, line in enumerate(lines):
if "解析结果:" in line and i+1 < len(lines):
if "解析结果:" in line and i + 1 < len(lines):
# 尝试解析后面的行
json_data = ""
for j in range(i+1, len(lines)):
for j in range(i + 1, len(lines)):
json_data += lines[j]
try:
return json.loads(json_data)
except json.JSONDecodeError:
continue
# 如果没有找到JSON数据则返回None
return None
except Exception as e:
print(f"从文件加载CycleInfo时出错: {e}")
return None
@staticmethod
def list_cycles(stream_id: str, base_dir: str = "log_debug") -> List[str]:
"""
列出指定stream_id的所有循环文件
参数:
stream_id: 聊天流ID
base_dir: 基础目录默认为log_debug
返回:
List[str]: 文件路径列表
"""
@@ -295,9 +298,12 @@ class CycleInfo:
stream_dir = os.path.join(base_dir, stream_id)
if not os.path.exists(stream_dir):
return []
files = [os.path.join(stream_dir, f) for f in os.listdir(stream_dir)
if f.startswith("cycle_") and f.endswith(".txt")]
files = [
os.path.join(stream_dir, f)
for f in os.listdir(stream_dir)
if f.startswith("cycle_") and f.endswith(".txt")
]
return sorted(files)
except Exception as e:
print(f"列出循环文件时出错: {e}")

File diff suppressed because it is too large Load Diff

View File

@@ -99,9 +99,13 @@ class HeartFCSender:
_ = message.update_thinking_time()
# --- 条件应用 set_reply 逻辑 ---
if message.apply_set_reply_logic and message.is_head and not message.is_private_message():
if (
message.is_head
and not message.is_private_message()
and message.reply.processed_plain_text != "[System Trigger Context]"
):
logger.debug(f"[{chat_id}] 应用 set_reply 逻辑: {message.processed_plain_text[:20]}...")
message.set_reply()
message.set_reply(message.reply)
# --- 结束条件 set_reply ---
await message.process()

View File

@@ -15,29 +15,51 @@ 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
from .heartFC_Cycleinfo import CycleDetail
logger = get_logger("prompt")
def init_prompt():
# Prompt(
# """
# {info_from_tools}
# {chat_target}
# {chat_talking_prompt}
# 现在你想要在群里发言或者回复。\n
# 你需要扮演一位网名叫{bot_name}的人进行回复,这个人的特点是:"{prompt_personality}"。
# 你正在{chat_target_2},现在请你读读之前的聊天记录,然后给出日常且口语化的回复,平淡一些,你可以参考贴吧,知乎或者微博的回复风格。
# 看到以上聊天记录,你刚刚在想:
# {current_mind_info}
# 因为上述想法,你决定发言,原因是:{reason}
# 依照这些内容组织回复:{in_mind_reply},不要原句回复,根据下面的要求,对其进行修改
# 要求:是尽量简短一些。把握聊天内容,{reply_style2}。不要复读自己说的话。{prompt_ger}
# {reply_style1},说中文,不要刻意突出自身学科背景。
# {moderation_prompt}。不要浮夸,平淡一些。
# 注意:回复不要输出多余内容(包括前后缀冒号和引号括号表情包at或 @等 )。""",
# "heart_flow_prompt",
# )
Prompt(
"""
{info_from_tools}
你可以参考以下的语言习惯:
当表示惊叹时,使用 我嘞个xxxx
当表示惊讶无语是,使用 不是?
当表示无语时,使用 阿这
当表示震惊时,使用 卧槽
{chat_target}
{chat_talking_prompt}
现在你想要在群里发言或者回复。\n
你需要扮演一位网名叫{bot_name}的人进行回复,这个人的特点是:"{prompt_personality}"
你正在{chat_target_2},现在请你读读之前的聊天记录,然后给出日常且口语化的回复,平淡一些,你可以参考贴吧,知乎或者微博的回复风格。
看到以上聊天记录,你刚刚在想:
{current_mind_info}
因为上述想法,你决定发言,原因是:{reason}
依照这些内容组织回复:{in_mind_reply},不要原句回复,根据下面的要求,对其进行修改
要求:是尽量简短一些。把握聊天内容,{reply_style2}。不要复读自己说的话。{prompt_ger}
{reply_style1},说中文,不要刻意突出自身学科背景。
{moderation_prompt}。不要浮夸,平淡一些。
你想表达:{in_mind_reply}
原因是:{reason}
请根据你想表达的内容,参考上述语言习惯,和下面的要求,给出回复
回复要求:
尽量简短一些。{reply_style2}{prompt_ger}
{reply_style1},说中文,不要刻意突出自身学科背景。不要浮夸,平淡一些。
注意:回复不要输出多余内容(包括前后缀冒号和引号括号表情包at或 @等 )。""",
"heart_flow_prompt",
)
@@ -71,14 +93,19 @@ def init_prompt():
2. 回复(reply)适用:
- 有实质性内容需要表达
- 有人提到你,但你还没有回应他
- 可以追加emoji_query表达情绪(emoji_query填写表情包的适用场合也就是当前场合)
- 不要追加太多表情
- 在合适的时候添加表情(不要总是添加)
- 如果你要回复特定某人的某句话或者你想回复较早的消息请在target中指定那句话的原始文本
3. 回复要求
3. 回复target选择
-如果选择了target不用特别提到某个人的人名
- 除非有明确的回复目标否则不要添加target
4. 回复要求:
-不要太浮夸
-一次只回复一个人
-一次只回复一个话题
4. 自我对话处理:
5. 自我对话处理:
- 如果是自己发的消息想继续,需自然衔接
- 避免重复或评价自己的发言
- 不要和自己聊天
@@ -95,8 +122,9 @@ def init_prompt():
如果选择reply请按以下JSON格式返回:
{{
"action": "reply",
"text": ["第一段文本", "第二段文本"], // 可选,如果想发送文本
"emojis": ["表情关键词1", "表情关键词2"] // 可选,如果想发送表情
"text": "你想表达的内容",
"emojis": "表情关键词",
"target": "你想要回复的原始文本内容(非必须,仅文本,不包含发送者)",
"reasoning": "你的决策理由",
}}
@@ -196,7 +224,9 @@ def init_prompt():
)
async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_stream, sender_name, in_mind_reply) -> str:
async def _build_prompt_focus(
reason, current_mind_info, structured_info, chat_stream, sender_name, in_mind_reply
) -> str:
individuality = Individuality.get_instance()
prompt_personality = individuality.get_prompt(x_person=0, level=2)
@@ -265,19 +295,20 @@ async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_s
prompt = await global_prompt_manager.format_prompt(
template_name,
info_from_tools=structured_info_prompt,
# info_from_tools=structured_info_prompt,
chat_target=chat_target_1, # Used in group template
chat_talking_prompt=chat_talking_prompt,
# chat_talking_prompt=chat_talking_prompt,
bot_name=global_config.BOT_NICKNAME,
prompt_personality=prompt_personality,
# prompt_personality=prompt_personality,
prompt_personality="",
chat_target_2=chat_target_2, # Used in group template
current_mind_info=current_mind_info,
# current_mind_info=current_mind_info,
reply_style2=reply_style2_chosen,
reply_style1=reply_style1_chosen,
reason=reason,
in_mind_reply=in_mind_reply,
prompt_ger=prompt_ger,
moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"),
# moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"),
# sender_name is not used in the group template
)
else: # Private chat
@@ -766,11 +797,11 @@ class PromptBuilder:
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],
cycle_info: Optional[str],
# replan_prompt: str, # Replan logic still simplified
) -> str:
"""构建 Planner LLM 的提示词 (获取模板并填充数据)"""
@@ -809,35 +840,6 @@ class PromptBuilder:
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)
@@ -857,7 +859,7 @@ class PromptBuilder:
structured_info_block=structured_info_block,
chat_content_block=chat_content_block,
current_mind_block=current_mind_block,
cycle_info_block=cycle_info_block,
cycle_info_block=cycle_info,
action_options_text=action_options_text,
# example_action=example_action_key,
)
@@ -870,9 +872,9 @@ class PromptBuilder:
async def build_planner_prompt_parallel(
self,
is_group_chat: bool,
is_group_chat: bool,
chat_target_info: Optional[dict],
cycle_history: Deque["CycleInfo"],
cycle_history: Deque["CycleDetail"],
observed_messages_str: str,
structured_info: str,
current_available_actions: Dict[str, str],
@@ -931,10 +933,10 @@ class PromptBuilder:
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
@@ -943,14 +945,14 @@ class PromptBuilder:
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:

View File

@@ -0,0 +1,44 @@
import time
import traceback
from typing import Optional
from src.plugins.chat.message import MessageRecv, BaseMessageInfo
from src.plugins.chat.chat_stream import ChatStream
from src.plugins.chat.message import UserInfo
from src.common.logger_manager import get_logger
logger = get_logger(__name__)
async def _create_empty_anchor_message(
platform: str, group_info: dict, chat_stream: ChatStream
) -> Optional[MessageRecv]:
"""
重构观察到的最后一条消息作为回复的锚点,
如果重构失败或观察为空,则创建一个占位符。
"""
try:
placeholder_id = f"mid_pf_{int(time.time() * 1000)}"
placeholder_user = UserInfo(user_id="system_trigger", user_nickname="System Trigger", platform=platform)
placeholder_msg_info = BaseMessageInfo(
message_id=placeholder_id,
platform=platform,
group_info=group_info,
user_info=placeholder_user,
time=time.time(),
)
placeholder_msg_dict = {
"message_info": placeholder_msg_info.to_dict(),
"processed_plain_text": "[System Trigger Context]",
"raw_message": "",
"time": placeholder_msg_info.time,
}
anchor_message = MessageRecv(placeholder_msg_dict)
anchor_message.update_chat_stream(chat_stream)
logger.debug(f"创建占位符锚点消息: ID={anchor_message.message_info.message_id}")
return anchor_message
except Exception as e:
logger.error(f"Error getting/creating anchor message: {e}")
logger.error(traceback.format_exc())
return None

View File

@@ -0,0 +1,48 @@
from abc import ABC, abstractmethod
from typing import List, Any, Optional
from src.heart_flow.info.info_base import InfoBase
from src.heart_flow.chatting_observation import Observation
from src.common.logger_manager import get_logger
logger = get_logger("base_processor")
class BaseProcessor(ABC):
"""信息处理器基类
所有具体的信息处理器都应该继承这个基类并实现process_info方法。
支持处理InfoBase和Observation类型的输入。
"""
@abstractmethod
def __init__(self):
"""初始化处理器"""
pass
@abstractmethod
async def process_info(
self, infos: List[InfoBase], observations: Optional[List[Observation]] = None, **kwargs: Any
) -> List[InfoBase]:
"""处理信息对象的抽象方法
Args:
infos: InfoBase对象列表
observations: 可选的Observation对象列表
**kwargs: 其他可选参数
Returns:
List[InfoBase]: 处理后的InfoBase实例列表
"""
pass
def _create_processed_item(self, info_type: str, info_data: Any) -> dict:
"""创建处理后的信息项
Args:
info_type: 信息类型
info_data: 信息数据
Returns:
dict: 处理后的信息项
"""
return {"type": info_type, "id": f"info_{info_type}", "content": info_data, "ttl": 3}

View File

@@ -0,0 +1,70 @@
from typing import List, Optional, Any
from src.heart_flow.info.obs_info import ObsInfo
from src.heart_flow.chatting_observation import Observation
from src.heart_flow.info.info_base import InfoBase
from .base_processor import BaseProcessor
from src.common.logger_manager import get_logger
from src.heart_flow.chatting_observation import ChattingObservation
from src.heart_flow.hfcloop_observation import HFCloopObservation
from src.heart_flow.info.cycle_info import CycleInfo
logger = get_logger("observation")
class ChattingInfoProcessor(BaseProcessor):
"""观察处理器
用于处理Observation对象将其转换为ObsInfo对象。
"""
def __init__(self):
"""初始化观察处理器"""
super().__init__()
async def process_info(self, observations: Optional[List[Observation]] = None, **kwargs: Any) -> List[InfoBase]:
"""处理Observation对象
Args:
infos: InfoBase对象列表
observations: 可选的Observation对象列表
**kwargs: 其他可选参数
Returns:
List[InfoBase]: 处理后的ObsInfo实例列表
"""
print(f"observations: {observations}")
processed_infos = []
# 处理Observation对象
if observations:
for obs in observations:
print(f"obs: {obs}")
if isinstance(obs, ChattingObservation):
obs_info = ObsInfo()
# 设置说话消息
if hasattr(obs, "talking_message_str"):
obs_info.set_talking_message(obs.talking_message_str)
# 设置截断后的说话消息
if hasattr(obs, "talking_message_str_truncate"):
obs_info.set_talking_message_str_truncate(obs.talking_message_str_truncate)
# 设置聊天类型
is_group_chat = obs.is_group_chat
if is_group_chat:
chat_type = "group"
else:
chat_type = "private"
obs_info.set_chat_target(obs.chat_target_info.get("person_name", "某人"))
obs_info.set_chat_type(chat_type)
logger.debug(f"聊天信息处理器处理后的信息: {obs_info}")
processed_infos.append(obs_info)
if isinstance(obs, HFCloopObservation):
obs_info = CycleInfo()
obs_info.set_observe_info(obs.observe_info)
processed_infos.append(obs_info)
return processed_infos

View File

@@ -0,0 +1,397 @@
from src.heart_flow.chatting_observation import ChattingObservation, Observation
from src.plugins.models.utils_model import LLMRequest
from src.config.config import global_config
import time
import traceback
from src.common.logger_manager import get_logger
from src.individuality.individuality import Individuality
import random
from src.plugins.utils.prompt_builder import Prompt, global_prompt_manager
from src.plugins.utils.json_utils import safe_json_dumps
from src.plugins.chat.chat_stream import chat_manager
import difflib
from src.plugins.person_info.relationship_manager import relationship_manager
from .base_processor import BaseProcessor
from src.heart_flow.info.mind_info import MindInfo
from typing import List, Optional
from src.heart_flow.memory_observation import MemoryObservation
from src.heart_flow.hfcloop_observation import HFCloopObservation
from src.plugins.heartFC_chat.info_processors.processor_utils import (
calculate_similarity,
calculate_replacement_probability,
get_spark,
)
logger = get_logger("sub_heartflow")
def init_prompt():
# --- Group Chat Prompt ---
group_prompt = """
{extra_info}
{relation_prompt}
你的名字是{bot_name},{prompt_personality},你现在{mood_info}
{cycle_info_block}
现在是{time_now}你正在上网和qq群里的网友们聊天以下是正在进行的聊天内容
{chat_observe_info}
以下是你之前对这个群聊的陈述:
{last_mind}
现在请你继续输出思考内容,输出要求:
1. 根据聊天内容生成你的想法,{hf_do_next}
2. 参考之前的思考,基于之前的内容对这个群聊继续陈述,可以删除不重要的内容,添加新的内容
3. 思考群内进行的话题,话题由谁发起,进展状况如何,你如何参与?思考你在群聊天中的角色,你是一个什么样的人,你在这个群聊中扮演什么角色?
4. 注意群聊的时间线索,思考聊天的时间线。
5. 请结合你做出的行为,对前面的陈述进行补充
6. 语言简洁自然,不要分点,不要浮夸,不要修辞,仅输出思考内容就好"""
Prompt(group_prompt, "sub_heartflow_prompt_before")
# --- Private Chat Prompt ---
private_prompt = """
{extra_info}
{relation_prompt}
你的名字是{bot_name},{prompt_personality},你现在{mood_info}
{cycle_info_block}
现在是{time_now},你正在上网,和 {chat_target_name} 私聊,以下是你们的聊天内容:
{chat_observe_info}
以下是你之前在这个群聊中的思考:
{last_mind}
请仔细阅读聊天内容,想想你和 {chat_target_name} 的关系,回顾你们刚刚的交流,你刚刚发言和对方的反应,思考聊天的主题。
请思考你要不要回复以及如何回复对方。
思考并输出你的内心想法
输出要求:
1. 根据聊天内容生成你的想法,{hf_do_next}
2. 不要分点、不要使用表情符号
3. 避免多余符号(冒号、引号、括号等)
4. 语言简洁自然,不要浮夸
5. 如果你刚发言,对方没有回复你,请谨慎回复"""
Prompt(private_prompt, "sub_heartflow_prompt_private_before")
class MindProcessor(BaseProcessor):
def __init__(self, subheartflow_id: str):
super().__init__()
self.subheartflow_id = subheartflow_id
self.llm_model = LLMRequest(
model=global_config.llm_sub_heartflow,
temperature=global_config.llm_sub_heartflow["temp"],
max_tokens=800,
request_type="sub_heart_flow",
)
self.current_mind = ""
self.past_mind = []
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", "")
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 process_info(self, observations: Optional[List[Observation]] = None, *infos) -> List[dict]:
"""处理信息对象
Args:
*infos: 可变数量的InfoBase类型的信息对象
Returns:
List[dict]: 处理后的结构化信息列表
"""
current_mind = await self.do_thinking_before_reply(observations)
mind_info = MindInfo()
mind_info.set_current_mind(current_mind)
return [mind_info]
async def do_thinking_before_reply(self, observations: Optional[List[Observation]] = None):
"""
在回复前进行思考,生成内心想法并收集工具调用结果
参数:
observations: 观察信息
返回:
如果return_prompt为False:
tuple: (current_mind, past_mind) 当前想法和过去的想法列表
如果return_prompt为True:
tuple: (current_mind, past_mind, prompt) 当前想法、过去的想法列表和使用的prompt
"""
# ---------- 0. 更新和清理 structured_info ----------
if self.structured_info:
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
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 ""
for observation in observations:
if isinstance(observation, ChattingObservation):
# 获取聊天元信息
is_group_chat = observation.is_group_chat
chat_target_info = observation.chat_target_info
chat_target_name = "对方" # 私聊默认名称
if not is_group_chat and chat_target_info:
# 优先使用person_name其次user_nickname最后回退到默认值
chat_target_name = (
chat_target_info.get("person_name") or chat_target_info.get("user_nickname") or chat_target_name
)
# 获取聊天内容
chat_observe_info = observation.get_observe_info()
person_list = observation.person_list
if isinstance(observation, MemoryObservation):
memory_observe_info = observation.get_observe_info()
if isinstance(observation, HFCloopObservation):
hfcloop_observe_info = observation.get_observe_info()
# ---------- 3. 准备个性化数据 ----------
# 获取个性化信息
individuality = Individuality.get_instance()
relation_prompt = ""
for person in person_list:
relation_prompt += await relationship_manager.build_relationship_info(person, is_id=True)
# 构建个性部分
prompt_personality = individuality.get_prompt(x_person=2, level=2)
# 获取当前时间
time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
spark_prompt = get_spark()
# ---------- 5. 构建最终提示词 ----------
template_name = "sub_heartflow_prompt_before" if is_group_chat else "sub_heartflow_prompt_private_before"
logger.debug(f"{self.log_prefix} 使用{'群聊' if is_group_chat else '私聊'}思考模板")
prompt = (await global_prompt_manager.get_prompt_async(template_name)).format(
extra_info=self.structured_info_str,
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=spark_prompt,
last_mind=previous_mind,
cycle_info_block=hfcloop_observe_info,
chat_target_name=chat_target_name,
)
# 在构建完提示词后生成最终的prompt字符串
final_prompt = prompt
content = "" # 初始化内容变量
try:
# 调用LLM生成响应
response, _ = await self.llm_model.generate_response_async(prompt=final_prompt)
# 直接使用LLM返回的文本响应作为 content
content = response if response else ""
except Exception as e:
# 处理总体异常
logger.error(f"{self.log_prefix} 执行LLM请求或处理响应时出错: {e}")
logger.error(traceback.format_exc())
content = "思考过程中出现错误"
# 记录初步思考结果
logger.debug(f"{self.log_prefix} 思考prompt: \n{final_prompt}\n")
# 处理空响应情况
if not content:
content = "(不知道该想些什么...)"
logger.warning(f"{self.log_prefix} LLM返回空结果思考失败。")
# ---------- 8. 更新思考状态并返回结果 ----------
logger.info(f"{self.log_prefix} 思考结果: {content}")
# 更新当前思考内容
self.update_current_mind(content)
return content
def update_current_mind(self, response):
if self.current_mind: # 只有当 current_mind 非空时才添加到 past_mind
self.past_mind.append(self.current_mind)
self.current_mind = response
def de_similar(self, previous_mind, new_content):
try:
similarity = calculate_similarity(previous_mind, new_content)
replacement_prob = calculate_replacement_probability(similarity)
logger.debug(f"{self.log_prefix} 新旧想法相似度: {similarity:.2f}, 替换概率: {replacement_prob:.2f}")
# 定义词语列表 (移到判断之前)
yu_qi_ci_liebiao = ["", "", "", "", "", ""]
zhuan_zhe_liebiao = ["但是", "不过", "然而", "可是", "只是"]
cheng_jie_liebiao = ["然后", "接着", "此外", "而且", "另外"]
zhuan_jie_ci_liebiao = zhuan_zhe_liebiao + cheng_jie_liebiao
if random.random() < replacement_prob:
# 相似度非常高时,尝试去重或特殊处理
if similarity == 1.0:
logger.debug(f"{self.log_prefix} 想法完全重复 (相似度 1.0),执行特殊处理...")
# 随机截取大约一半内容
if len(new_content) > 1: # 避免内容过短无法截取
split_point = max(
1, len(new_content) // 2 + random.randint(-len(new_content) // 4, len(new_content) // 4)
)
truncated_content = new_content[:split_point]
else:
truncated_content = new_content # 如果只有一个字符或者为空,就不截取了
# 添加语气词和转折/承接词
yu_qi_ci = random.choice(yu_qi_ci_liebiao)
zhuan_jie_ci = random.choice(zhuan_jie_ci_liebiao)
content = f"{yu_qi_ci}{zhuan_jie_ci}{truncated_content}"
logger.debug(f"{self.log_prefix} 想法重复,特殊处理后: {content}")
else:
# 相似度较高但非100%,执行标准去重逻辑
logger.debug(f"{self.log_prefix} 执行概率性去重 (概率: {replacement_prob:.2f})...")
logger.debug(
f"{self.log_prefix} previous_mind类型: {type(previous_mind)}, new_content类型: {type(new_content)}"
)
matcher = difflib.SequenceMatcher(None, previous_mind, new_content)
logger.debug(f"{self.log_prefix} matcher类型: {type(matcher)}")
deduplicated_parts = []
last_match_end_in_b = 0
# 获取并记录所有匹配块
matching_blocks = matcher.get_matching_blocks()
logger.debug(f"{self.log_prefix} 匹配块数量: {len(matching_blocks)}")
logger.debug(
f"{self.log_prefix} 匹配块示例(前3个): {matching_blocks[:3] if len(matching_blocks) > 3 else matching_blocks}"
)
# get_matching_blocks()返回形如[(i, j, n), ...]的列表其中i是a中的索引j是b中的索引n是匹配的长度
for idx, match in enumerate(matching_blocks):
if not isinstance(match, tuple):
logger.error(f"{self.log_prefix} 匹配块 {idx} 不是元组类型,而是 {type(match)}: {match}")
continue
try:
_i, j, n = match # 解包元组为三个变量
logger.debug(f"{self.log_prefix} 匹配块 {idx}: i={_i}, j={j}, n={n}")
if last_match_end_in_b < j:
# 确保添加的是字符串,而不是元组
try:
non_matching_part = new_content[last_match_end_in_b:j]
logger.debug(
f"{self.log_prefix} 添加非匹配部分: '{non_matching_part}', 类型: {type(non_matching_part)}"
)
if not isinstance(non_matching_part, str):
logger.warning(
f"{self.log_prefix} 非匹配部分不是字符串类型: {type(non_matching_part)}"
)
non_matching_part = str(non_matching_part)
deduplicated_parts.append(non_matching_part)
except Exception as e:
logger.error(f"{self.log_prefix} 处理非匹配部分时出错: {e}")
logger.error(traceback.format_exc())
last_match_end_in_b = j + n
except Exception as e:
logger.error(f"{self.log_prefix} 处理匹配块时出错: {e}")
logger.error(traceback.format_exc())
logger.debug(f"{self.log_prefix} 去重前部分列表: {deduplicated_parts}")
logger.debug(f"{self.log_prefix} 列表元素类型: {[type(part) for part in deduplicated_parts]}")
# 确保所有元素都是字符串
deduplicated_parts = [str(part) for part in deduplicated_parts]
# 防止列表为空
if not deduplicated_parts:
logger.warning(f"{self.log_prefix} 去重后列表为空,添加空字符串")
deduplicated_parts = [""]
logger.debug(f"{self.log_prefix} 处理后的部分列表: {deduplicated_parts}")
try:
deduplicated_content = "".join(deduplicated_parts).strip()
logger.debug(f"{self.log_prefix} 拼接后的去重内容: '{deduplicated_content}'")
except Exception as e:
logger.error(f"{self.log_prefix} 拼接去重内容时出错: {e}")
logger.error(traceback.format_exc())
deduplicated_content = ""
if deduplicated_content:
# 根据概率决定是否添加词语
prefix_str = ""
if random.random() < 0.3: # 30% 概率添加语气词
prefix_str += random.choice(yu_qi_ci_liebiao)
if random.random() < 0.7: # 70% 概率添加转折/承接词
prefix_str += random.choice(zhuan_jie_ci_liebiao)
# 组合最终结果
if prefix_str:
content = f"{prefix_str}{deduplicated_content}" # 更新 content
logger.debug(f"{self.log_prefix} 去重并添加引导词后: {content}")
else:
content = deduplicated_content # 更新 content
logger.debug(f"{self.log_prefix} 去重后 (未添加引导词): {content}")
else:
logger.warning(f"{self.log_prefix} 去重后内容为空保留原始LLM输出: {new_content}")
content = new_content # 保留原始 content
else:
logger.debug(f"{self.log_prefix} 未执行概率性去重 (概率: {replacement_prob:.2f})")
# content 保持 new_content 不变
except Exception as e:
logger.error(f"{self.log_prefix} 应用概率性去重或特殊处理时出错: {e}")
logger.error(traceback.format_exc())
# 出错时保留原始 content
content = new_content
return content
init_prompt()

View File

@@ -0,0 +1,56 @@
import difflib
import random
import time
def calculate_similarity(text_a: str, text_b: str) -> float:
"""
计算两个文本字符串的相似度。
"""
if not text_a or not text_b:
return 0.0
matcher = difflib.SequenceMatcher(None, text_a, text_b)
return matcher.ratio()
def calculate_replacement_probability(similarity: float) -> float:
"""
根据相似度计算替换的概率。
规则:
- 相似度 <= 0.4: 概率 = 0
- 相似度 >= 0.9: 概率 = 1
- 相似度 == 0.6: 概率 = 0.7
- 0.4 < 相似度 <= 0.6: 线性插值 (0.4, 0) 到 (0.6, 0.7)
- 0.6 < 相似度 < 0.9: 线性插值 (0.6, 0.7) 到 (0.9, 1.0)
"""
if similarity <= 0.4:
return 0.0
elif similarity >= 0.9:
return 1.0
elif 0.4 < similarity <= 0.6:
# p = 3.5 * s - 1.4
probability = 3.5 * similarity - 1.4
return max(0.0, probability)
else: # 0.6 < similarity < 0.9
# p = s + 0.1
probability = similarity + 0.1
return min(1.0, max(0.0, probability))
def get_spark():
local_random = random.Random()
current_minute = int(time.strftime("%M"))
local_random.seed(current_minute)
hf_options = [
("可以参考之前的想法,在原来想法的基础上继续思考", 0.2),
("可以参考之前的想法,在原来的想法上尝试新的话题", 0.4),
("不要太深入", 0.2),
("进行深入思考", 0.2),
]
# 加权随机选择思考指导
hf_do_next = local_random.choices(
[option[0] for option in hf_options], weights=[option[1] for option in hf_options], k=1
)[0]
return hf_do_next

View File

@@ -0,0 +1,200 @@
from src.heart_flow.chatting_observation import ChattingObservation
from src.plugins.models.utils_model import LLMRequest
from src.config.config import global_config
import time
from src.common.logger_manager import get_logger
from src.individuality.individuality import Individuality
from src.plugins.utils.prompt_builder import Prompt, global_prompt_manager
from src.do_tool.tool_use import ToolUser
from src.plugins.utils.json_utils import process_llm_tool_calls
from src.plugins.person_info.relationship_manager import relationship_manager
from .base_processor import BaseProcessor
from typing import List, Optional
from src.heart_flow.chatting_observation import Observation
from src.heart_flow.working_observation import WorkingObservation
from src.heart_flow.info.structured_info import StructuredInfo
logger = get_logger("tool_use")
def init_prompt():
# ... 原有代码 ...
# 添加工具执行器提示词
tool_executor_prompt = """
你是一个专门执行工具的助手。你的名字是{bot_name}。现在是{time_now}
你要在群聊中扮演以下角色:
{prompt_personality}
你当前的额外信息:
{extra_info}
你的心情是:{mood_info}
{relation_prompt}
群里正在进行的聊天内容:
{chat_observe_info}
请仔细分析聊天内容,考虑以下几点:
1. 内容中是否包含需要查询信息的问题
2. 是否需要执行特定操作
3. 是否有明确的工具使用指令
4. 考虑用户与你的关系以及当前的对话氛围
如果需要使用工具,请直接调用相应的工具函数。如果不需要使用工具,请简单输出"无需使用工具"
尽量只在确实必要时才使用工具。
"""
Prompt(tool_executor_prompt, "tool_executor_prompt")
class ToolProcessor(BaseProcessor):
def __init__(self, subheartflow_id: str):
super().__init__()
self.subheartflow_id = subheartflow_id
self.log_prefix = f"[{subheartflow_id}:ToolExecutor] "
self.llm_model = LLMRequest(
model=global_config.llm_normal,
max_tokens=500,
request_type="tool_execution",
)
self.structured_info = []
async def process_info(self, observations: Optional[List[Observation]] = None, *infos) -> List[dict]:
"""处理信息对象
Args:
*infos: 可变数量的InfoBase类型的信息对象
Returns:
list: 处理后的结构化信息列表
"""
if observations:
for observation in observations:
if isinstance(observation, ChattingObservation):
result, used_tools, prompt = await self.execute_tools(observation)
# 更新WorkingObservation中的结构化信息
for observation in observations:
if isinstance(observation, WorkingObservation):
for structured_info in result:
logger.debug(f"{self.log_prefix} 更新WorkingObservation中的结构化信息: {structured_info}")
observation.add_structured_info(structured_info)
working_infos = observation.get_observe_info()
logger.debug(f"{self.log_prefix} 获取更新后WorkingObservation中的结构化信息: {working_infos}")
structured_info = StructuredInfo()
for working_info in working_infos:
structured_info.set_info(working_info.get("type"), working_info.get("content"))
return [structured_info]
async def execute_tools(self, observation: ChattingObservation):
"""
并行执行工具,返回结构化信息
参数:
sub_mind: 子思维对象
chat_target_name: 聊天目标名称,默认为"对方"
is_group_chat: 是否为群聊默认为False
return_details: 是否返回详细信息默认为False
cycle_info: 循环信息对象,可用于记录详细执行信息
返回:
如果return_details为False:
List[Dict]: 工具执行结果的结构化信息列表
如果return_details为True:
Tuple[List[Dict], List[str], str]: (工具执行结果列表, 使用的工具列表, 工具执行提示词)
"""
tool_instance = ToolUser()
tools = tool_instance._define_tools()
logger.debug(f"observation: {observation}")
logger.debug(f"observation.chat_target_info: {observation.chat_target_info}")
logger.debug(f"observation.is_group_chat: {observation.is_group_chat}")
logger.debug(f"observation.person_list: {observation.person_list}")
is_group_chat = observation.is_group_chat
if not is_group_chat:
chat_target_name = (
observation.chat_target_info.get("person_name")
or observation.chat_target_info.get("user_nickname")
or "对方"
)
else:
chat_target_name = "群聊"
chat_observe_info = observation.get_observe_info()
person_list = observation.person_list
# 构建关系信息
relation_prompt = "【关系信息】\n"
for person in person_list:
relation_prompt += await relationship_manager.build_relationship_info(person, is_id=True)
# 获取个性信息
individuality = Individuality.get_instance()
prompt_personality = individuality.get_prompt(x_person=2, level=2)
# 获取心情信息
mood_info = observation.chat_state.mood if hasattr(observation, "chat_state") else ""
# 获取时间信息
time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
# 构建专用于工具调用的提示词
prompt = await global_prompt_manager.format_prompt(
"tool_executor_prompt",
extra_info="extra_structured_info",
chat_observe_info=chat_observe_info,
# chat_target_name=chat_target_name,
is_group_chat=is_group_chat,
relation_prompt=relation_prompt,
prompt_personality=prompt_personality,
mood_info=mood_info,
bot_name=individuality.name,
time_now=time_now,
)
# 调用LLM专注于工具使用
logger.info(f"开始执行工具调用{prompt}")
response, _, tool_calls = await self.llm_model.generate_response_tool_async(prompt=prompt, tools=tools)
logger.debug(f"获取到工具原始输出:\n{tool_calls}")
# 处理工具调用和结果收集类似于SubMind中的逻辑
new_structured_items = []
used_tools = [] # 记录使用了哪些工具
if tool_calls:
success, valid_tool_calls, error_msg = process_llm_tool_calls(tool_calls)
if success and valid_tool_calls:
for tool_call in valid_tool_calls:
try:
# 记录使用的工具名称
tool_name = tool_call.get("name", "unknown_tool")
used_tools.append(tool_name)
result = await tool_instance._execute_tool_call(tool_call)
name = result.get("type", "unknown_type")
content = result.get("content", "")
logger.info(f"工具{name},获得信息:{content}")
if result:
new_item = {
"type": result.get("type", "unknown_type"),
"id": result.get("id", f"tool_exec_{time.time()}"),
"content": result.get("content", ""),
"ttl": 3,
}
new_structured_items.append(new_item)
except Exception as e:
logger.error(f"{self.log_prefix}工具执行失败: {e}")
return new_structured_items, used_tools, prompt
init_prompt()

View File

@@ -352,6 +352,9 @@ class NormalChat:
# --- 新增:处理初始高兴趣消息的私有方法 ---
async def _process_initial_interest_messages(self):
"""处理启动时存在于 interest_dict 中的高兴趣消息。"""
if not self.interest_dict:
return # 如果 interest_dict 为 None 或空,直接返回
items_to_process = list(self.interest_dict.items())
if not items_to_process:
return # 没有初始消息,直接返回