This commit is contained in:
SengokuCola
2025-05-01 22:50:29 +08:00
parent c4a7b842f6
commit 2669572b30
9 changed files with 220 additions and 188 deletions

View File

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

View File

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

View File

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

View File

@@ -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 = "" # 初始化内容变量

View File

@@ -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。"
)
# --- 结束新增 --- #

View File

@@ -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
return is_group_chat, chat_target_info