feat: 增加vtb_action,修复吞消息,ruff

This commit is contained in:
tcmofashi
2025-06-01 18:21:34 +08:00
parent 565d3ae43e
commit 0e2465075e
16 changed files with 326 additions and 202 deletions

View File

@@ -1,9 +1,10 @@
import time
import os
from typing import List, Optional, Dict, Any
from typing import Optional, Dict, Any
log_dir = "log/log_cycle_debug/"
class CycleDetail:
"""循环信息记录类"""
@@ -23,35 +24,40 @@ class CycleDetail:
def to_dict(self) -> Dict[str, Any]:
"""将循环信息转换为字典格式"""
def convert_to_serializable(obj, depth=0, seen=None):
if seen is None:
seen = set()
# 防止递归过深
if depth > 5: # 降低递归深度限制
return str(obj)
# 防止循环引用
obj_id = id(obj)
if obj_id in seen:
return str(obj)
seen.add(obj_id)
try:
if hasattr(obj, 'to_dict'):
if hasattr(obj, "to_dict"):
# 对于有to_dict方法的对象直接调用其to_dict方法
return obj.to_dict()
elif isinstance(obj, dict):
# 对于字典,只保留基本类型和可序列化的值
return {k: convert_to_serializable(v, depth + 1, seen)
for k, v in obj.items()
if isinstance(k, (str, int, float, bool))}
return {
k: convert_to_serializable(v, depth + 1, seen)
for k, v in obj.items()
if isinstance(k, (str, int, float, bool))
}
elif isinstance(obj, (list, tuple)):
# 对于列表和元组,只保留可序列化的元素
return [convert_to_serializable(item, depth + 1, seen)
for item in obj
if not isinstance(item, (dict, list, tuple)) or
isinstance(item, (str, int, float, bool, type(None)))]
return [
convert_to_serializable(item, depth + 1, seen)
for item in obj
if not isinstance(item, (dict, list, tuple))
or isinstance(item, (str, int, float, bool, type(None)))
]
elif isinstance(obj, (str, int, float, bool, type(None))):
return obj
else:
@@ -74,19 +80,19 @@ class CycleDetail:
def complete_cycle(self):
"""完成循环,记录结束时间"""
self.end_time = time.time()
# 处理 prefix只保留中英文字符
if not self.prefix:
self.prefix = "group"
else:
# 只保留中文和英文字符
self.prefix = ''.join(char for char in self.prefix if '\u4e00' <= char <= '\u9fff' or char.isascii())
self.prefix = "".join(char for char in self.prefix if "\u4e00" <= char <= "\u9fff" or char.isascii())
if not self.prefix:
self.prefix = "group"
current_time_minute = time.strftime("%Y%m%d_%H%M", time.localtime())
self.log_cycle_to_file(log_dir + self.prefix + f"/{current_time_minute}_cycle_" + str(self.cycle_id) + ".json")
def log_cycle_to_file(self, file_path: str):
"""将循环信息写入文件"""
# 如果目录不存在,则创建目录
@@ -95,6 +101,7 @@ class CycleDetail:
os.makedirs(dir_name, exist_ok=True)
# 写入文件
import json
with open(file_path, "a", encoding="utf-8") as f:
f.write(json.dumps(self.to_dict(), ensure_ascii=False) + "\n")

View File

@@ -418,7 +418,9 @@ class HeartFChatting:
# 记录耗时
processor_time_costs[processor_name] = duration_since_parallel_start
except asyncio.TimeoutError:
logger.info(f"{self.log_prefix} 处理器 {processor_name} 超时(>{global_config.focus_chat.processor_max_time}s已跳过")
logger.info(
f"{self.log_prefix} 处理器 {processor_name} 超时(>{global_config.focus_chat.processor_max_time}s已跳过"
)
processor_time_costs[processor_name] = global_config.focus_chat.processor_max_time
except Exception as e:
logger.error(
@@ -462,7 +464,7 @@ class HeartFChatting:
}
self.all_observations = observations
with Timer("调整动作", cycle_timers):
# 处理特殊的观察
await self.action_modifier.modify_actions(observations=observations)
@@ -476,26 +478,24 @@ class HeartFChatting:
with Timer("并行回忆和处理", cycle_timers):
memory_task = asyncio.create_task(self.memory_activator.activate_memory(observations))
processor_task = asyncio.create_task(self._process_processors(observations, []))
# 等待两个任务完成
running_memorys, (all_plan_info, processor_time_costs) = await asyncio.gather(memory_task, processor_task)
running_memorys, (all_plan_info, processor_time_costs) = await asyncio.gather(
memory_task, processor_task
)
else:
# 串行执行
with Timer("回忆", cycle_timers):
running_memorys = await self.memory_activator.activate_memory(observations)
with Timer("执行 信息处理器", cycle_timers):
all_plan_info, processor_time_costs = await self._process_processors(
observations, running_memorys
)
all_plan_info, processor_time_costs = await self._process_processors(observations, running_memorys)
loop_processor_info = {
"all_plan_info": all_plan_info,
"processor_time_costs": processor_time_costs,
}
with Timer("规划器", cycle_timers):
plan_result = await self.action_planner.plan(all_plan_info, running_memorys)

View File

@@ -30,7 +30,6 @@ class ActionModifier:
observations: Optional[List[Observation]] = None,
**kwargs: Any,
):
# 处理Observation对象
if observations:
# action_info = ActionInfo()
@@ -163,22 +162,34 @@ class ActionModifier:
if len(last_max_reply_num) >= max_reply_num and all(last_max_reply_num):
# 如果最近max_reply_num次都是reply直接移除
result["remove"].append("reply")
logger.info(f"最近{len(last_max_reply_num)}次回复中,有{no_reply_count}次no_reply{len(last_max_reply_num) - no_reply_count}次reply直接移除")
logger.info(
f"最近{len(last_max_reply_num)}次回复中,有{no_reply_count}次no_reply{len(last_max_reply_num) - no_reply_count}次reply直接移除"
)
elif len(last_max_reply_num) >= sec_thres_reply_num and all(last_max_reply_num[-sec_thres_reply_num:]):
# 如果最近sec_thres_reply_num次都是reply40%概率移除
if random.random() < 0.4 / global_config.focus_chat.consecutive_replies:
result["remove"].append("reply")
logger.info(f"最近{len(last_max_reply_num)}次回复中,有{no_reply_count}次no_reply{len(last_max_reply_num) - no_reply_count}次reply{0.4 / global_config.focus_chat.consecutive_replies}概率移除,移除")
logger.info(
f"最近{len(last_max_reply_num)}次回复中,有{no_reply_count}次no_reply{len(last_max_reply_num) - no_reply_count}次reply{0.4 / global_config.focus_chat.consecutive_replies}概率移除,移除"
)
else:
logger.debug(f"最近{len(last_max_reply_num)}次回复中,有{no_reply_count}次no_reply{len(last_max_reply_num) - no_reply_count}次reply{0.4 / global_config.focus_chat.consecutive_replies}概率移除,不移除")
logger.debug(
f"最近{len(last_max_reply_num)}次回复中,有{no_reply_count}次no_reply{len(last_max_reply_num) - no_reply_count}次reply{0.4 / global_config.focus_chat.consecutive_replies}概率移除,不移除"
)
elif len(last_max_reply_num) >= one_thres_reply_num and all(last_max_reply_num[-one_thres_reply_num:]):
# 如果最近one_thres_reply_num次都是reply20%概率移除
if random.random() < 0.2 / global_config.focus_chat.consecutive_replies:
result["remove"].append("reply")
logger.info(f"最近{len(last_max_reply_num)}次回复中,有{no_reply_count}次no_reply{len(last_max_reply_num) - no_reply_count}次reply{0.2 / global_config.focus_chat.consecutive_replies}概率移除,移除")
logger.info(
f"最近{len(last_max_reply_num)}次回复中,有{no_reply_count}次no_reply{len(last_max_reply_num) - no_reply_count}次reply{0.2 / global_config.focus_chat.consecutive_replies}概率移除,移除"
)
else:
logger.debug(f"最近{len(last_max_reply_num)}次回复中,有{no_reply_count}次no_reply{len(last_max_reply_num) - no_reply_count}次reply{0.2 / global_config.focus_chat.consecutive_replies}概率移除,不移除")
logger.debug(
f"最近{len(last_max_reply_num)}次回复中,有{no_reply_count}次no_reply{len(last_max_reply_num) - no_reply_count}次reply{0.2 / global_config.focus_chat.consecutive_replies}概率移除,不移除"
)
else:
logger.debug(f"最近{len(last_max_reply_num)}次回复中,有{no_reply_count}次no_reply{len(last_max_reply_num) - no_reply_count}次reply无需移除")
logger.debug(
f"最近{len(last_max_reply_num)}次回复中,有{no_reply_count}次no_reply{len(last_max_reply_num) - no_reply_count}次reply无需移除"
)
return result

View File

@@ -29,14 +29,6 @@ def init_prompt():
{self_info_block}
{extra_info_block}
{memory_str}
你需要基于以下信息决定如何参与对话
这些信息可能会有冲突请你整合这些信息并选择一个最合适的action
{chat_content_block}
{mind_info_block}
{cycle_info_block}
请综合分析聊天内容和你看到的新消息参考聊天规划选择合适的action:
注意除了下面动作选项之外你在群聊里不能做其他任何事情这是你能力的边界现在请你选择合适的action:
{action_options_text}
@@ -46,6 +38,15 @@ def init_prompt():
{moderation_prompt}
你需要基于以下信息决定如何参与对话
这些信息可能会有冲突请你整合这些信息并选择一个最合适的action
{chat_content_block}
{mind_info_block}
{cycle_info_block}
请综合分析聊天内容和你看到的新消息参考聊天规划选择合适的action:
请你以下面格式输出你选择的action
{{
"action": "action_name",
@@ -270,7 +271,6 @@ class ActionPlanner:
) -> str:
"""构建 Planner LLM 的提示词 (获取模板并填充数据)"""
try:
memory_str = ""
if global_config.focus_chat.parallel_processing:
memory_str = ""
@@ -278,10 +278,7 @@ class ActionPlanner:
memory_str = "以下是当前在聊天中,你回忆起的记忆:\n"
for running_memory in running_memorys:
memory_str += f"{running_memory['topic']}: {running_memory['content']}\n"
chat_context_description = "你现在正在一个群聊中"
chat_target_name = None # Only relevant for private
if not is_group_chat and chat_target_info:

View File

@@ -42,5 +42,5 @@ class ActionObservation:
"observe_id": self.observe_id,
"last_observe_time": self.last_observe_time,
"all_actions": self.all_actions,
"all_using_actions": self.all_using_actions
"all_using_actions": self.all_using_actions,
}

View File

@@ -81,7 +81,7 @@ class ChattingObservation(Observation):
"person_list": self.person_list,
"oldest_messages_str": self.oldest_messages_str,
"compressor_prompt": self.compressor_prompt,
"last_observe_time": self.last_observe_time
"last_observe_time": self.last_observe_time,
}
async def initialize(self):
@@ -89,6 +89,8 @@ class ChattingObservation(Observation):
logger.debug(f"初始化observation: self.is_group_chat: {self.is_group_chat}")
logger.debug(f"初始化observation: self.chat_target_info: {self.chat_target_info}")
initial_messages = get_raw_msg_before_timestamp_with_chat(self.chat_id, self.last_observe_time, 10)
self.last_observe_time = initial_messages[-1]["time"] if initial_messages else self.last_observe_time
# logger.error(f"初始化observation: initial_messages: {initial_messages}\n\n\n\n{self.last_observe_time}")
self.talking_message = initial_messages
self.talking_message_str = await build_readable_messages(self.talking_message)

View File

@@ -39,7 +39,7 @@ class HFCloopObservation:
responses_for_prompt = []
cycle_last_reason = ""
# 检查这最近的活动循环中有多少是连续的文本回复 (从最近的开始看)
for cycle in recent_active_cycles:
action_type = cycle.loop_plan_info["action_result"]["action_type"]
@@ -57,29 +57,33 @@ class HFCloopObservation:
action_reasoning_str = f"你选择这个action的原因是:{action_reasoning}"
else:
action_reasoning_str = ""
if action_type == "reply":
consecutive_text_replies += 1
response_text = cycle.loop_plan_info["action_result"]["action_data"].get("text", "[空回复]")
responses_for_prompt.append(response_text)
if is_taken:
action_detailed_str += f"{action_taken_time_str}时,你选择回复(action:{action_type},内容是:'{response_text}')。{action_reasoning_str}\n"
else:
action_detailed_str += f"{action_taken_time_str}时,你选择回复(action:{action_type},内容是:'{response_text}'),但是动作失败了。{action_reasoning_str}\n"
elif action_type == "no_reply":
action_detailed_str += f"{action_taken_time_str}时,你选择不回复(action:{action_type}){action_reasoning_str}\n"
action_detailed_str += (
f"{action_taken_time_str}时,你选择不回复(action:{action_type}){action_reasoning_str}\n"
)
else:
if is_taken:
action_detailed_str += f"{action_taken_time_str}时,你选择执行了(action:{action_type}){action_reasoning_str}\n"
action_detailed_str += (
f"{action_taken_time_str}时,你选择执行了(action:{action_type}){action_reasoning_str}\n"
)
else:
action_detailed_str += f"{action_taken_time_str}时,你选择执行了(action:{action_type}),但是动作失败了。{action_reasoning_str}\n"
if action_detailed_str:
cycle_info_block = f"\n你最近做的事:\n{action_detailed_str}\n"
else:
cycle_info_block = "\n"
# 根据连续文本回复的数量构建提示信息
if consecutive_text_replies >= 3: # 如果最近的三个活动都是文本回复
cycle_info_block = f'你已经连续回复了三条消息(最近: "{responses_for_prompt[0]}",第二近: "{responses_for_prompt[1]}",第三近: "{responses_for_prompt[2]}")。你回复的有点多了,请注意'
@@ -116,5 +120,5 @@ class HFCloopObservation:
"observe_id": self.observe_id,
"last_observe_time": self.last_observe_time,
# 不序列化history_loop避免循环引用
"history_loop_count": len(self.history_loop)
"history_loop_count": len(self.history_loop),
}

View File

@@ -18,7 +18,7 @@ class Observation:
return {
"observe_info": self.observe_info,
"observe_id": self.observe_id,
"last_observe_time": self.last_observe_time
"last_observe_time": self.last_observe_time,
}
async def observe(self):

View File

@@ -22,7 +22,7 @@ class StructureObservation:
"observe_id": self.observe_id,
"last_observe_time": self.last_observe_time,
"history_loop": self.history_loop,
"structured_info": self.structured_info
"structured_info": self.structured_info,
}
def get_observe_info(self):

View File

@@ -39,6 +39,10 @@ class WorkingMemoryObservation:
"observe_info": self.observe_info,
"observe_id": self.observe_id,
"last_observe_time": self.last_observe_time,
"working_memory": self.working_memory.to_dict() if hasattr(self.working_memory, 'to_dict') else str(self.working_memory),
"retrieved_working_memory": [item.to_dict() if hasattr(item, 'to_dict') else str(item) for item in self.retrieved_working_memory]
"working_memory": self.working_memory.to_dict()
if hasattr(self.working_memory, "to_dict")
else str(self.working_memory),
"retrieved_working_memory": [
item.to_dict() if hasattr(item, "to_dict") else str(item) for item in self.retrieved_working_memory
],
}

View File

@@ -146,10 +146,10 @@ class FocusChatConfig(ConfigBase):
consecutive_replies: float = 1
"""连续回复能力,值越高,麦麦连续回复的概率越高"""
parallel_processing: bool = False
"""是否允许处理器阶段和回忆阶段并行执行"""
processor_max_time: int = 25
"""处理器最大时间,单位秒,如果超过这个时间,处理器会自动停止"""
@@ -166,13 +166,11 @@ class FocusChatProcessorConfig(ConfigBase):
working_memory_processor: bool = True
"""是否启用工作记忆处理器"""
lite_chat_mind_processor: bool = False
"""是否启用轻量级聊天思维处理器可以节省token消耗和时间"""
@dataclass
class ExpressionConfig(ConfigBase):
"""表达配置类"""

View File

@@ -753,7 +753,7 @@ class LLMRequest:
response = await self._execute_request(endpoint="/chat/completions", payload=data, prompt=prompt)
# 原样返回响应,不做处理
if len(response) == 3:
content, reasoning_content, tool_calls = response
return content, (reasoning_content, self.model_name, tool_calls)

View File

View File

@@ -0,0 +1 @@
from . import vtb_action # noqa

View File

@@ -0,0 +1,74 @@
from src.common.logger_manager import get_logger
from src.chat.focus_chat.planners.actions.plugin_action import PluginAction, register_action
from typing import Tuple
logger = get_logger("vtb_action")
@register_action
class VTBAction(PluginAction):
"""VTB虚拟主播动作处理类"""
action_name = "vtb_action"
action_description = "使用虚拟主播预设动作表达心情或感觉,适用于需要生动表达情感的场景"
action_parameters = {
"text": "描述想要表达的心情或感觉的文本内容,必填,应当是对情感状态的自然描述",
}
action_require = [
"当需要表达特定情感或心情时使用",
"当用户明确要求使用虚拟主播动作时使用",
"当回应内容需要更生动的情感表达时使用",
"当想要通过预设动作增强互动体验时使用",
]
default = True # 设为默认动作
associated_types = ["vtb_text"]
async def process(self) -> Tuple[bool, str]:
"""处理VTB虚拟主播动作"""
logger.info(f"{self.log_prefix} 执行VTB动作: {self.reasoning}")
# 获取要表达的心情或感觉文本
text = self.action_data.get("text")
if not text:
logger.error(f"{self.log_prefix} 执行VTB动作时未提供文本内容")
return False, "执行VTB动作失败未提供文本内容"
# 处理文本使其更适合VTB动作表达
processed_text = self._process_text_for_vtb(text)
try:
# 发送VTB动作消息
await self.send_message(type="vtb_text", data=processed_text)
logger.info(f"{self.log_prefix} VTB动作执行成功文本内容: {processed_text}")
return True, "VTB动作执行成功"
except Exception as e:
logger.error(f"{self.log_prefix} 执行VTB动作时出错: {e}")
return False, f"执行VTB动作时出错: {e}"
def _process_text_for_vtb(self, text: str) -> str:
"""
处理文本使其更适合VTB动作表达
- 优化情感表达的准确性
- 规范化心情描述格式
- 确保文本适合虚拟主播动作系统理解
"""
# 简单示例实现
processed_text = text.strip()
# 移除多余的空格和换行
import re
processed_text = re.sub(r"\s+", " ", processed_text)
# 确保文本长度适中,避免过长的描述
if len(processed_text) > 100:
processed_text = processed_text[:100] + "..."
# 如果文本为空,提供默认的情感描述
if not processed_text:
processed_text = "平静"
return processed_text