feat:为HFC创建私聊特殊prompt模板

This commit is contained in:
SengokuCola
2025-05-01 21:38:38 +08:00
parent d97aa6b115
commit 462fac2547
9 changed files with 417 additions and 121 deletions

View File

@@ -22,7 +22,7 @@ logger = get_logger("config")
# 考虑到实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码 # 考虑到实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码
is_test = False is_test = False
mai_version_main = "0.6.3" mai_version_main = "0.6.3"
mai_version_fix = "fix-1" mai_version_fix = "fix-2"
if mai_version_fix: if mai_version_fix:
if is_test: if is_test:

View File

@@ -19,6 +19,8 @@ INTEREST_EVAL_INTERVAL_SECONDS = 5
NORMAL_CHAT_TIMEOUT_CHECK_INTERVAL_SECONDS = 60 NORMAL_CHAT_TIMEOUT_CHECK_INTERVAL_SECONDS = 60
# 新增状态评估间隔 # 新增状态评估间隔
HF_JUDGE_STATE_UPDATE_INTERVAL_SECONDS = 60 HF_JUDGE_STATE_UPDATE_INTERVAL_SECONDS = 60
# 新增私聊激活检查间隔
PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS = 5 # 与兴趣评估类似设为5秒
CLEANUP_INTERVAL_SECONDS = 1200 CLEANUP_INTERVAL_SECONDS = 1200
STATE_UPDATE_INTERVAL_SECONDS = 60 STATE_UPDATE_INTERVAL_SECONDS = 60
@@ -71,9 +73,10 @@ class BackgroundTaskManager:
self._state_update_task: Optional[asyncio.Task] = None self._state_update_task: Optional[asyncio.Task] = None
self._cleanup_task: Optional[asyncio.Task] = None self._cleanup_task: Optional[asyncio.Task] = None
self._logging_task: Optional[asyncio.Task] = None self._logging_task: Optional[asyncio.Task] = None
self._normal_chat_timeout_check_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 # Nyaa~ 添加状态评估任务的引用 self._hf_judge_state_update_task: Optional[asyncio.Task] = None
self._into_focus_task: Optional[asyncio.Task] = None # Nyaa~ 添加兴趣评估任务的引用 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 self._tasks: List[Optional[asyncio.Task]] = [] # Keep track of all tasks
async def start_tasks(self): async def start_tasks(self):
@@ -124,6 +127,14 @@ class BackgroundTaskManager:
f"专注评估任务已启动 间隔:{INTEREST_EVAL_INTERVAL_SECONDS}s", f"专注评估任务已启动 间隔:{INTEREST_EVAL_INTERVAL_SECONDS}s",
"_into_focus_task", "_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, interval=INTEREST_EVAL_INTERVAL_SECONDS,
task_func=self._perform_into_focus_work, 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
)

View File

@@ -12,9 +12,31 @@ from src.plugins.utils.chat_message_builder import (
num_new_messages_since, num_new_messages_since,
get_person_id_list, 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") 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: class Observation:
@@ -34,28 +56,37 @@ class ChattingObservation(Observation):
super().__init__("chat", chat_id) super().__init__("chat", chat_id)
self.chat_id = 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 = []
self.talking_message_str = "" self.talking_message_str = ""
self.talking_message_str_truncate = "" self.talking_message_str_truncate = ""
self.name = global_config.BOT_NICKNAME self.name = global_config.BOT_NICKNAME
self.nick_name = global_config.BOT_ALIAS_NAMES self.nick_name = global_config.BOT_ALIAS_NAMES
self.max_now_obs_len = global_config.observation_context_size self.max_now_obs_len = global_config.observation_context_size
self.overlap_len = global_config.compressed_length self.overlap_len = global_config.compressed_length
self.mid_memorys = [] self.mid_memorys = []
self.max_mid_memory_len = global_config.compress_length_limit self.max_mid_memory_len = global_config.compress_length_limit
self.mid_memory_info = "" self.mid_memory_info = ""
self.person_list = [] self.person_list = []
self.llm_summary = LLMRequest( self.llm_summary = LLMRequest(
model=global_config.llm_observation, temperature=0.7, max_tokens=300, request_type="chat_observation" model=global_config.llm_observation, temperature=0.7, max_tokens=300, request_type="chat_observation"
) )
async def initialize(self): 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) 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) self.talking_message_str = await build_readable_messages(self.talking_message)
# 进行一次观察 返回观察结果observe_info # 进行一次观察 返回观察结果observe_info
@@ -109,18 +140,49 @@ class ChattingObservation(Observation):
messages=oldest_messages, timestamp_mode="normal", read_mark=0 messages=oldest_messages, timestamp_mode="normal", read_mark=0
) )
# 调用 LLM 总结主题 # --- Build prompt using template ---
prompt = ( prompt = None # Initialize prompt as None
f"请总结以下聊天记录的主题:\n{oldest_messages_str}\n用一句话概括包括人物事件和主要信息,不要分点:"
)
summary = "没有主题的闲聊" # 默认值
try: try:
summary_result, _ = await self.llm_summary.generate_response_async(prompt) # 构建 Prompt - 根据 is_group_chat 选择模板
if summary_result: # 确保结果不为空 if self.is_group_chat:
summary = summary_result 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: 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 = { mid_memory = {
"id": str(int(datetime.now().timestamp())), "id": str(int(datetime.now().timestamp())),

View File

@@ -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.mai_state_manager import MaiStateInfo
from src.heart_flow.chat_state_info import ChatState, ChatStateInfo from src.heart_flow.chat_state_info import ChatState, ChatStateInfo
from src.heart_flow.sub_mind import SubMind from src.heart_flow.sub_mind import SubMind
from src.plugins.person_info.person_info import person_info_manager
from .utils_chat import get_chat_type_and_target_info
# 定义常量 (从 interest.py 移动过来) # 定义常量 (从 interest.py 移动过来)
@@ -238,6 +240,11 @@ class SubHeartflow:
self.chat_state_last_time: float = 0 self.chat_state_last_time: float = 0
self.history_chat_state: List[Tuple[ChatState, float]] = [] 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() self.interest_chatting: InterestChatting = InterestChatting()
@@ -260,11 +267,20 @@ class SubHeartflow:
subheartflow_id=self.subheartflow_id, chat_state=self.chat_state, observations=self.observations subheartflow_id=self.subheartflow_id, chat_state=self.chat_state, observations=self.observations
) )
# 日志前缀 # 日志前缀 - Moved determination to initialize
self.log_prefix = chat_manager.get_stream_name(self.subheartflow_id) or self.subheartflow_id self.log_prefix = str(subheartflow_id) # Initial default prefix
async def initialize(self): 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() await self.interest_chatting.initialize()
logger.debug(f"{self.log_prefix} InterestChatting 实例已初始化。") logger.debug(f"{self.log_prefix} InterestChatting 实例已初始化。")
@@ -286,26 +302,33 @@ class SubHeartflow:
async def _start_normal_chat(self) -> bool: async def _start_normal_chat(self) -> bool:
""" """
启动 NormalChat 实例, 启动 NormalChat 实例,并进行异步初始化。
进入 CHAT 状态时使用 进入 CHAT 状态时使用
确保 HeartFChatting 已停止。
确保 HeartFChatting 已停止
""" """
await self._stop_heart_fc_chat() # 确保 专注聊天已停止 await self._stop_heart_fc_chat() # 确保 专注聊天已停止
log_prefix = self.log_prefix log_prefix = self.log_prefix
try: try:
# 获取聊天流并创建 NormalChat 实例 # 获取聊天流并创建 NormalChat 实例 (同步部分)
chat_stream = chat_manager.get_stream(self.chat_id) 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()) 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} 开始普通聊天,随便水群...") 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 return True
except Exception as e: except Exception as e:
logger.error(f"{log_prefix} 启动 NormalChat 时出错: {e}") logger.error(f"{log_prefix} 启动 NormalChat 或其初始化时出错: {e}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
self.normal_chat_instance = None # 启动失败,清理实例 self.normal_chat_instance = None # 启动/初始化失败,清理实例
return False return False
async def _stop_heart_fc_chat(self): async def _stop_heart_fc_chat(self):

View File

@@ -335,27 +335,35 @@ class SubHeartflowManager:
async def sbhf_absent_into_chat(self): async def sbhf_absent_into_chat(self):
""" """
随机选一个 ABSENT 状态的子心流,评估是否应转换为 CHAT 状态。 随机选一个 ABSENT 状态的 *群聊* 子心流,评估是否应转换为 CHAT 状态。
每次调用最多转换一个。 每次调用最多转换一个。
私聊会被忽略。
""" """
current_mai_state = self.mai_state_info.get_current_state() current_mai_state = self.mai_state_info.get_current_state()
chat_limit = current_mai_state.get_normal_chat_max_num() chat_limit = current_mai_state.get_normal_chat_max_num()
async with self._lock: async with self._lock:
# 1. 筛选出所有 ABSENT 状态的子心流 # 1. 筛选出所有 ABSENT 状态的 *群聊* 子心流
absent_subflows = [ absent_group_subflows = [
hf for hf in self.subheartflows.values() if hf.chat_state.chat_status == ChatState.ABSENT hf for hf in self.subheartflows.values()
if hf.chat_state.chat_status == ChatState.ABSENT and hf.is_group_chat
] ]
if not absent_subflows: if not absent_group_subflows:
logger.debug("没有摸鱼的子心流可以评估。") # 日志太频繁,注释掉 # logger.debug("没有摸鱼的群聊子心流可以评估。") # 日志太频繁
return # 没有目标,直接返回 return # 没有目标,直接返回
# 2. 随机选一个幸运儿 # 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 flow_id = sub_hf_to_evaluate.subheartflow_id
stream_name = chat_manager.get_stream_name(flow_id) or flow_id stream_name = chat_manager.get_stream_name(flow_id) or flow_id
log_prefix = f"[{stream_name}]" log_prefix = f"[{stream_name}]"
# --- 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 上限 # 3. 检查 CHAT 上限
current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT) current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT)
@@ -658,8 +666,10 @@ class SubHeartflowManager:
# --- 新增:处理来自 HeartFChatting 的状态转换请求 --- # # --- 新增:处理来自 HeartFChatting 的状态转换请求 --- #
async def sbhf_focus_into_absent(self, subflow_id: Any): async def sbhf_focus_into_absent(self, subflow_id: Any):
""" """
接收来自 HeartFChatting 的请求,将特定子心流的状态转换为 ABSENT。 接收来自 HeartFChatting 的请求,将特定子心流的状态转换为 ABSENT 或 CHAT
通常在连续多次 "no_reply" 后被调用。 通常在连续多次 "no_reply" 后被调用。
对于私聊,总是转换为 ABSENT。
对于群聊,随机决定转换为 ABSENT 或 CHAT (如果 CHAT 未达上限)。
Args: Args:
subflow_id: 需要转换状态的子心流 ID。 subflow_id: 需要转换状态的子心流 ID。
@@ -667,50 +677,44 @@ class SubHeartflowManager:
async with self._lock: async with self._lock:
subflow = self.subheartflows.get(subflow_id) subflow = self.subheartflows.get(subflow_id)
if not subflow: if not subflow:
logger.warning(f"[状态转换请求] 尝试转换不存在的子心流 {subflow_id} 到 ABSENT") logger.warning(f"[状态转换请求] 尝试转换不存在的子心流 {subflow_id} 到 ABSENT/CHAT")
return return
stream_name = chat_manager.get_stream_name(subflow_id) or subflow_id stream_name = chat_manager.get_stream_name(subflow_id) or subflow_id
current_state = subflow.chat_state.chat_status current_state = subflow.chat_state.chat_status
# 仅当子心流处于 FOCUSED 状态时才进行转换
# 因为 HeartFChatting 只在 FOCUSED 状态下运行
if current_state == ChatState.FOCUSED: if current_state == ChatState.FOCUSED:
target_state = ChatState.ABSENT # 默认目标状态 target_state = ChatState.ABSENT # Default target
log_reason = "默认转换" log_reason = "默认转换 (私聊或群聊)"
# 决定是去 ABSENT 还是 CHAT # --- Modify logic based on chat type --- #
if random.random() < 0.5: if subflow.is_group_chat:
target_state = ChatState.ABSENT # Group chat: Decide between ABSENT or CHAT
log_reason = "随机选择 ABSENT" if random.random() < 0.5: # 50% chance to try CHAT
logger.debug(f"[状态转换请求] {stream_name} ({current_state.value}) 随机决定进入 ABSENT") current_mai_state = self.mai_state_info.get_current_state()
else: chat_limit = current_mai_state.get_normal_chat_max_num()
# 尝试进入 CHAT先检查限制 current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT)
current_mai_state = self.mai_state_info.get_current_state()
chat_limit = current_mai_state.get_normal_chat_max_num() if current_chat_count < chat_limit:
# 使用不上锁的版本,因为我们已经在锁内 target_state = ChatState.CHAT
current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT) log_reason = f"群聊随机选择 CHAT (当前 {current_chat_count}/{chat_limit})"
else:
if current_chat_count < chat_limit: target_state = ChatState.ABSENT # Fallback to ABSENT if CHAT limit reached
target_state = ChatState.CHAT log_reason = f"群聊随机选择 CHAT 但已达上限 ({current_chat_count}/{chat_limit}),转为 ABSENT"
log_reason = f"随机选择 CHAT (当前 {current_chat_count}/{chat_limit})" else: # 50% chance to go directly to ABSENT
logger.debug(
f"[状态转换请求] {stream_name} ({current_state.value}) 随机决定进入 CHAT未达上限 ({current_chat_count}/{chat_limit})"
)
else:
target_state = ChatState.ABSENT target_state = ChatState.ABSENT
log_reason = f"随机选择 CHAT 但已达上限 ({current_chat_count}/{chat_limit}),转为 ABSENT" log_reason = "群聊随机选择 ABSENT"
logger.debug( else:
f"[状态转换请求] {stream_name} ({current_state.value}) 随机决定进入 CHAT但已达上限 ({current_chat_count}/{chat_limit}),改为进入 ABSENT" # Private chat: Always go to ABSENT
) target_state = ChatState.ABSENT
log_reason = "私聊退出 FOCUSED转为 ABSENT"
# --- End modification --- #
# 开始转换
logger.info( logger.info(
f"[状态转换请求] 接收到请求,将 {stream_name} (当前: {current_state.value}) 尝试转换为 {target_state.value} ({log_reason})" f"[状态转换请求] 接收到请求,将 {stream_name} (当前: {current_state.value}) 尝试转换为 {target_state.value} ({log_reason})"
) )
try: try:
await subflow.change_chat_state(target_state) await subflow.change_chat_state(target_state)
# 检查最终状态
final_state = subflow.chat_state.chat_status final_state = subflow.chat_state.chat_status
if final_state == target_state: if final_state == target_state:
logger.debug(f"[状态转换请求] {stream_name} 状态已成功转换为 {final_state.value}") logger.debug(f"[状态转换请求] {stream_name} 状态已成功转换为 {final_state.value}")
@@ -728,5 +732,98 @@ class SubHeartflowManager:
logger.warning( logger.warning(
f"[状态转换请求] 收到对 {stream_name} 的请求,但其状态为 {current_state.value} (非 FOCUSED),不执行转换" 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 的状态转换请求 --- #

View File

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

View File

@@ -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.respon_info_catcher.info_catcher import info_catcher_manager
from src.plugins.moods.moods import MoodManager from src.plugins.moods.moods import MoodManager
from src.individuality.individuality import Individuality from src.individuality.individuality import Individuality
from src.heart_flow.utils_chat import get_chat_type_and_target_info
WAITING_TIME_THRESHOLD = 300 # 等待新消息时间阈值,单位秒 WAITING_TIME_THRESHOLD = 300 # 等待新消息时间阈值,单位秒
@@ -194,7 +195,12 @@ class HeartFChatting:
self.on_consecutive_no_reply_callback = on_consecutive_no_reply_callback 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() self.action_manager = ActionManager()
@@ -234,22 +240,34 @@ class HeartFChatting:
async def _initialize(self) -> bool: async def _initialize(self) -> bool:
""" """
懒初始化以使用提供的标识符解析chat_stream。 懒初始化解析chat_stream, 获取聊天类型和目标信息
确保实例已准备好处理触发器。
""" """
if self._initialized: if self._initialized:
return True return True
self.chat_stream = chat_manager.get_stream(self.stream_id) # --- Use utility function to determine chat type and fetch info ---
if not self.chat_stream: # Note: get_chat_type_and_target_info handles getting the chat_stream internally
logger.error(f"{self.log_prefix} 获取ChatStream失败。") self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.stream_id)
return False
# 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
self.log_prefix = f"[{chat_manager.get_stream_name(self.stream_id) or self.stream_id}]" # 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 self._initialized = True
logger.debug(f"{self.log_prefix}麦麦感觉到了,可以开始认真水群 ") logger.debug(f"{self.log_prefix} 麦麦感觉到了,可以开始认真水群 ")
return True return True
async def start(self): async def start(self):

View File

@@ -79,7 +79,7 @@ def init_prompt():
- 避免重复或评价自己的发言 - 避免重复或评价自己的发言
- 不要和自己聊天 - 不要和自己聊天
决策任务 决策任务
{action_options_text} {action_options_text}
你必须从上面列出的可用行动中选择一个,并说明原因。 你必须从上面列出的可用行动中选择一个,并说明原因。
@@ -90,20 +90,6 @@ JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query":
"reasoning": "string", // 做出此决定的详细理由和思考过程,说明你如何应用了回复原则 "reasoning": "string", // 做出此决定的详细理由和思考过程,说明你如何应用了回复原则
"emoji_query": "string" // 可选。如果行动是 'emoji_reply',必须提供表情主题(填写表情包的适用场合);如果行动是 'text_reply' 且你想附带表情,也在此提供表情主题,否则留空字符串 ""。遵循回复原则,不要滥用。 "emoji_query": "string" // 可选。如果行动是 'emoji_reply',必须提供表情主题(填写表情包的适用场合);如果行动是 'text_reply' 且你想附带表情,也在此提供表情主题,否则留空字符串 ""。遵循回复原则,不要滥用。
}} }}
例如:
{{
"action": "text_reply",
"reasoning": "用户提到了我,且问题比较具体,适合用文本回复。考虑到内容,可以带上一个微笑表情。",
"emoji_query": "微笑"
}}
{{
"action": "no_reply",
"reasoning": "我已经连续回复了两次,而且这个话题我不太感兴趣,根据回复原则,选择不回复,等待其他人发言。",
"emoji_query": ""
}}
请输出你的决策 JSON 请输出你的决策 JSON
""", # 使用三引号避免内部引号问题 """, # 使用三引号避免内部引号问题
"planner_prompt", # 保持名称不变,替换内容 "planner_prompt", # 保持名称不变,替换内容

View File

@@ -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.person_info.relationship_manager import relationship_manager
from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
from src.plugins.utils.timer_calculator import Timer 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") logger = get_logger("chat")
@@ -26,28 +27,46 @@ logger = get_logger("chat")
class NormalChat: class NormalChat:
def __init__(self, chat_stream: ChatStream, interest_dict: dict): def __init__(self, chat_stream: ChatStream, interest_dict: dict):
""" """初始化 NormalChat 实例。只进行同步操作。"""
初始化 NormalChat 实例,针对特定的 ChatStream。
Args:
chat_stream (ChatStream): 此 NormalChat 实例关联的聊天流对象。
"""
# Basic info from chat_stream (sync)
self.chat_stream = chat_stream self.chat_stream = chat_stream
self.stream_id = chat_stream.stream_id 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 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.gpt = NormalChatGenerator()
self.mood_manager = MoodManager.get_instance() # MoodManager 保持单例 self.mood_manager = MoodManager.get_instance()
# 存储此实例的兴趣监控任务
self.start_time = time.time() self.start_time = time.time()
self.last_speak_time = 0 self.last_speak_time = 0
self._chat_task: Optional[asyncio.Task] = None self._chat_task: Optional[asyncio.Task] = None
logger.info(f"[{self.stream_name}] NormalChat 实例初始化完成。") 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: async def _create_thinking_message(self, message: MessageRecv) -> str:
@@ -416,22 +435,18 @@ class NormalChat:
# 改为实例方法, 移除 chat 参数 # 改为实例方法, 移除 chat 参数
async def start_chat(self): 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(): if self._chat_task is None or self._chat_task.done():
# --- 修改:使用 create_task 启动初始消息处理 --- logger.info(f"[{self.stream_name}] 开始后台处理初始兴趣消息和轮询任务...")
logger.info(f"[{self.stream_name}] 开始后台处理初始兴趣消息...") # Process initial messages first
# 创建一个任务来处理初始消息,不阻塞当前流程 await self._process_initial_interest_messages()
_initial_process_task = asyncio.create_task(self._process_initial_interest_messages()) # Then start polling task
# 可以考虑给这个任务也添加完成回调来记录日志或处理错误 polling_task = asyncio.create_task(self._reply_interested_message())
# initial_process_task.add_done_callback(...)
# --- 修改结束 ---
# 启动后台轮询任务 (这部分不变)
logger.info(f"[{self.stream_name}] 启动后台兴趣消息轮询任务...")
polling_task = asyncio.create_task(self._reply_interested_message()) # 注意变量名区分
polling_task.add_done_callback(lambda t: self._handle_task_completion(t)) 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: else:
logger.info(f"[{self.stream_name}] 聊天轮询任务已在运行中。") logger.info(f"[{self.stream_name}] 聊天轮询任务已在运行中。")