重构Kokoro Flow Chatter:移除已弃用的响应后处理器和会话管理器
- 删除了`response_post_processor.py`和`session_manager.py`,因为它们已不再需要。 - 更新了`__init__.py`文件,移除了对`ActionExecutor`的引用。 - 删除了`action_executor.py`,并将动作执行直接集成到`chatter.py`和`proactive_thinker.py`中。 - 在`KokoroFlowChatterV2`中重构了动作执行逻辑,以直接使用`ChatterActionManager`。 - 增强了主动思考逻辑,以简化操作执行,而无需依赖已移除的`ActionExecutor`。
This commit is contained in:
@@ -4,12 +4,10 @@ import traceback
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
from src.chat.utils.timer_calculator import Timer
|
||||
from src.common.data_models.database_data_model import DatabaseMessages
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import global_config
|
||||
from src.person_info.person_info import get_person_info_manager
|
||||
from src.plugin_system.apis import database_api, generator_api, message_api, send_api
|
||||
from src.plugin_system.apis import database_api
|
||||
from src.plugin_system.base.base_action import BaseAction
|
||||
from src.plugin_system.base.component_types import ActionInfo, ComponentType
|
||||
from src.plugin_system.core.component_registry import component_registry
|
||||
@@ -160,6 +158,8 @@ class ChatterActionManager:
|
||||
) -> Any:
|
||||
"""
|
||||
执行单个动作的通用函数
|
||||
|
||||
所有动作(包括 reply/respond)都通过 BaseAction.execute() 执行
|
||||
|
||||
Args:
|
||||
action_name: 动作名称
|
||||
@@ -169,14 +169,13 @@ class ChatterActionManager:
|
||||
action_data: 动作数据
|
||||
thinking_id: 思考ID
|
||||
log_prefix: 日志前缀
|
||||
clear_unread_messages: 是否清除未读消息
|
||||
|
||||
Returns:
|
||||
执行结果
|
||||
"""
|
||||
|
||||
chat_stream = None
|
||||
try:
|
||||
logger.debug(f"🎯 [ActionManager] execute_action接收到 target_message: {target_message}")
|
||||
# 通过chat_id获取chat_stream
|
||||
chat_manager = get_chat_manager()
|
||||
chat_stream = await chat_manager.get_stream(chat_id)
|
||||
@@ -193,149 +192,33 @@ class ChatterActionManager:
|
||||
# 设置正在回复的状态
|
||||
chat_stream.context.is_replying = True
|
||||
|
||||
# no_action 特殊处理
|
||||
if action_name == "no_action":
|
||||
return {"action_type": "no_action", "success": True, "reply_text": "", "command": ""}
|
||||
|
||||
if action_name == "no_reply":
|
||||
# 直接处理no_reply逻辑,不再通过动作系统
|
||||
reason = reasoning or "选择不回复"
|
||||
logger.info(f"{log_prefix} 选择不回复,原因: {reason}")
|
||||
# 统一通过 _handle_action 执行所有动作
|
||||
success, reply_text, command = await self._handle_action(
|
||||
chat_stream,
|
||||
action_name,
|
||||
reasoning,
|
||||
action_data or {},
|
||||
{}, # cycle_timers
|
||||
thinking_id,
|
||||
target_message,
|
||||
)
|
||||
|
||||
# 存储no_reply信息到数据库(支持批量存储)
|
||||
if self._batch_storage_enabled:
|
||||
self.add_action_to_batch(
|
||||
action_name="no_reply",
|
||||
action_data={"reason": reason},
|
||||
thinking_id=thinking_id or "",
|
||||
action_done=True,
|
||||
action_build_into_prompt=False,
|
||||
action_prompt_display=reason,
|
||||
)
|
||||
else:
|
||||
asyncio.create_task(database_api.store_action_info(
|
||||
chat_stream=chat_stream,
|
||||
action_build_into_prompt=False,
|
||||
action_prompt_display=reason,
|
||||
action_done=True,
|
||||
thinking_id=thinking_id or "",
|
||||
action_data={"reason": reason},
|
||||
action_name="no_reply",
|
||||
))
|
||||
# 记录执行的动作到目标消息
|
||||
if success:
|
||||
asyncio.create_task(self._record_action_to_message(chat_stream, action_name, target_message, action_data))
|
||||
# 重置打断计数
|
||||
await self._reset_interruption_count_after_action(chat_stream.stream_id)
|
||||
|
||||
return {"action_type": "no_reply", "success": True, "reply_text": "", "command": ""}
|
||||
|
||||
elif action_name != "reply" and action_name != "respond" and action_name != "no_action":
|
||||
# 执行普通动作
|
||||
success, reply_text, command = await self._handle_action(
|
||||
chat_stream,
|
||||
action_name,
|
||||
reasoning,
|
||||
action_data or {},
|
||||
{}, # cycle_timers
|
||||
thinking_id,
|
||||
target_message,
|
||||
)
|
||||
|
||||
# 记录执行的动作到目标消息
|
||||
if success:
|
||||
asyncio.create_task(self._record_action_to_message(chat_stream, action_name, target_message, action_data))
|
||||
# 重置打断计数
|
||||
await self._reset_interruption_count_after_action(chat_stream.stream_id)
|
||||
|
||||
return {
|
||||
"action_type": action_name,
|
||||
"success": success,
|
||||
"reply_text": reply_text,
|
||||
"command": command,
|
||||
}
|
||||
else:
|
||||
# 检查目标消息是否为表情包消息以及配置是否允许回复表情包
|
||||
if target_message and getattr(target_message, "is_emoji", False):
|
||||
# 如果是表情包消息且配置不允许回复表情包,则跳过回复
|
||||
if not getattr(global_config.chat, "allow_reply_to_emoji", True):
|
||||
logger.info(f"{log_prefix} 目标消息为表情包且配置不允许回复表情包,跳过回复")
|
||||
return {"action_type": action_name, "success": True, "reply_text": "", "skip_reason": "emoji_not_allowed"}
|
||||
|
||||
# 生成回复 (reply 或 respond)
|
||||
# reply: 针对单条消息的回复,使用 s4u 模板
|
||||
# respond: 对未读消息的统一回应,使用 normal 模板
|
||||
try:
|
||||
# 根据动作类型确定提示词模式
|
||||
prompt_mode = "s4u" if action_name == "reply" else "normal"
|
||||
|
||||
# 将prompt_mode传递给generate_reply
|
||||
action_data_with_mode = (action_data or {}).copy()
|
||||
action_data_with_mode["prompt_mode"] = prompt_mode
|
||||
|
||||
# 只传递当前正在执行的动作,而不是所有可用动作
|
||||
# 这样可以让LLM明确知道"已决定执行X动作",而不是"有这些动作可用"
|
||||
current_action_info = self._using_actions.get(action_name)
|
||||
current_actions: dict[str, Any] = {action_name: current_action_info} if current_action_info else {}
|
||||
|
||||
# 附加目标消息信息(如果存在)
|
||||
if target_message:
|
||||
# 提取目标消息的关键信息
|
||||
target_msg_info = {
|
||||
"message_id": getattr(target_message, "message_id", ""),
|
||||
"sender": getattr(target_message.user_info, "user_nickname", "") if hasattr(target_message, "user_info") else "",
|
||||
"content": getattr(target_message, "processed_plain_text", ""),
|
||||
"time": getattr(target_message, "time", 0),
|
||||
}
|
||||
current_actions["_target_message"] = target_msg_info
|
||||
|
||||
success, response_set, _ = await generator_api.generate_reply(
|
||||
chat_stream=chat_stream,
|
||||
reply_message=target_message,
|
||||
action_data=action_data_with_mode,
|
||||
available_actions=current_actions, # type: ignore
|
||||
enable_tool=global_config.tool.enable_tool,
|
||||
request_type="chat.replyer",
|
||||
from_plugin=False,
|
||||
)
|
||||
if not success or not response_set:
|
||||
# 安全地获取 processed_plain_text
|
||||
if target_message:
|
||||
msg_text = target_message.processed_plain_text or "未知消息"
|
||||
else:
|
||||
msg_text = "未知消息"
|
||||
|
||||
logger.info(f"对 {msg_text} 的回复生成失败")
|
||||
return {"action_type": action_name, "success": False, "reply_text": "", "loop_info": None}
|
||||
except asyncio.CancelledError:
|
||||
logger.debug(f"{log_prefix} 并行执行:回复生成任务已被取消")
|
||||
return {"action_type": action_name, "success": False, "reply_text": "", "loop_info": None}
|
||||
|
||||
# 从action_data中提取should_quote_reply参数
|
||||
should_quote_reply = None
|
||||
if action_data and isinstance(action_data, dict):
|
||||
should_quote_reply = action_data.get("should_quote_reply", None)
|
||||
|
||||
# respond动作默认不引用回复,保持对话流畅
|
||||
if action_name == "respond" and should_quote_reply is None:
|
||||
should_quote_reply = False
|
||||
|
||||
async def _after_reply():
|
||||
# 发送并存储回复
|
||||
reply_text, cycle_timers_reply = await self._send_and_store_reply(
|
||||
chat_stream,
|
||||
response_set,
|
||||
asyncio.get_event_loop().time(),
|
||||
target_message,
|
||||
{}, # cycle_timers
|
||||
thinking_id,
|
||||
[], # actions
|
||||
should_quote_reply, # 传递should_quote_reply参数
|
||||
)
|
||||
|
||||
# 记录回复动作到目标消息
|
||||
await self._record_action_to_message(chat_stream, action_name, target_message, action_data)
|
||||
|
||||
# 回复成功,重置打断计数
|
||||
await self._reset_interruption_count_after_action(chat_stream.stream_id)
|
||||
|
||||
return reply_text
|
||||
asyncio.create_task(_after_reply())
|
||||
return {"action_type": action_name, "success": True}
|
||||
return {
|
||||
"action_type": action_name,
|
||||
"success": success,
|
||||
"reply_text": reply_text,
|
||||
"command": command,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"{log_prefix} 执行动作时出错: {e}")
|
||||
|
||||
@@ -57,6 +57,7 @@ class ActionModifier:
|
||||
async def modify_actions(
|
||||
self,
|
||||
message_content: str = "",
|
||||
chatter_name: str = "",
|
||||
): # sourcery skip: use-named-expression
|
||||
"""
|
||||
动作修改流程,整合传统观察处理和新的激活类型判定
|
||||
@@ -66,6 +67,10 @@ class ActionModifier:
|
||||
2. 基于激活类型的智能动作判定,最终确定可用动作集
|
||||
|
||||
处理后,ActionManager 将包含最终的可用动作集,供规划器直接使用
|
||||
|
||||
Args:
|
||||
message_content: 消息内容
|
||||
chatter_name: 当前使用的 Chatter 名称,用于过滤只允许特定 Chatter 使用的动作
|
||||
"""
|
||||
# 初始化log_prefix
|
||||
await self._initialize_log_prefix()
|
||||
@@ -82,13 +87,14 @@ class ActionModifier:
|
||||
|
||||
logger.debug(f"{self.log_prefix}开始完整动作修改流程")
|
||||
|
||||
removals_s0: list[tuple[str, str]] = [] # 第0阶段:聊天类型和Chatter过滤
|
||||
removals_s1: list[tuple[str, str]] = []
|
||||
removals_s2: list[tuple[str, str]] = []
|
||||
removals_s3: list[tuple[str, str]] = []
|
||||
|
||||
all_actions = self.action_manager.get_using_actions()
|
||||
|
||||
# === 第0阶段:根据聊天类型过滤动作 ===
|
||||
# === 第0阶段:根据聊天类型和Chatter过滤动作 ===
|
||||
from src.chat.utils.utils import get_chat_type_and_target_info
|
||||
from src.plugin_system.base.component_types import ChatType, ComponentType
|
||||
from src.plugin_system.core.component_registry import component_registry
|
||||
@@ -97,26 +103,35 @@ class ActionModifier:
|
||||
is_group_chat, _ = await get_chat_type_and_target_info(self.chat_id)
|
||||
all_registered_actions = component_registry.get_components_by_type(ComponentType.ACTION)
|
||||
|
||||
chat_type_removals = []
|
||||
for action_name in list(all_actions.keys()):
|
||||
if action_name in all_registered_actions:
|
||||
action_info = all_registered_actions[action_name]
|
||||
|
||||
# 检查聊天类型限制
|
||||
chat_type_allow = getattr(action_info, "chat_type_allow", ChatType.ALL)
|
||||
|
||||
# 检查是否符合聊天类型限制
|
||||
should_keep = (
|
||||
should_keep_chat_type = (
|
||||
chat_type_allow == ChatType.ALL
|
||||
or (chat_type_allow == ChatType.GROUP and is_group_chat)
|
||||
or (chat_type_allow == ChatType.PRIVATE and not is_group_chat)
|
||||
)
|
||||
|
||||
if not should_keep:
|
||||
chat_type_removals.append((action_name, f"不支持{'群聊' if is_group_chat else '私聊'}"))
|
||||
|
||||
if not should_keep_chat_type:
|
||||
removals_s0.append((action_name, f"不支持{'群聊' if is_group_chat else '私聊'}"))
|
||||
self.action_manager.remove_action_from_using(action_name)
|
||||
continue
|
||||
|
||||
# 检查 Chatter 限制
|
||||
chatter_allow = getattr(action_info, "chatter_allow", [])
|
||||
if chatter_allow and chatter_name:
|
||||
# 如果设置了 chatter_allow 且提供了 chatter_name,则检查是否匹配
|
||||
if chatter_name not in chatter_allow:
|
||||
removals_s0.append((action_name, f"仅限 {', '.join(chatter_allow)} 使用"))
|
||||
self.action_manager.remove_action_from_using(action_name)
|
||||
continue
|
||||
|
||||
if chat_type_removals:
|
||||
logger.info(f"{self.log_prefix} 第0阶段:根据聊天类型过滤 - 移除了 {len(chat_type_removals)} 个动作")
|
||||
for action_name, reason in chat_type_removals:
|
||||
if removals_s0:
|
||||
logger.info(f"{self.log_prefix} 第0阶段:类型/Chatter过滤 - 移除了 {len(removals_s0)} 个动作")
|
||||
for action_name, reason in removals_s0:
|
||||
logger.debug(f"{self.log_prefix} - 移除 {action_name}: {reason}")
|
||||
|
||||
message_list_before_now_half = await get_raw_msg_before_timestamp_with_chat(
|
||||
|
||||
@@ -559,6 +559,7 @@ class BaseAction(ABC):
|
||||
action_require=getattr(cls, "action_require", []).copy(),
|
||||
associated_types=getattr(cls, "associated_types", []).copy(),
|
||||
chat_type_allow=getattr(cls, "chat_type_allow", ChatType.ALL),
|
||||
chatter_allow=getattr(cls, "chatter_allow", []).copy(),
|
||||
# 二步Action相关属性
|
||||
is_two_step_action=getattr(cls, "is_two_step_action", False),
|
||||
step_one_description=getattr(cls, "step_one_description", ""),
|
||||
|
||||
@@ -209,6 +209,7 @@ class ActionInfo(ComponentInfo):
|
||||
mode_enable: ChatMode = ChatMode.ALL
|
||||
parallel_action: bool = False
|
||||
chat_type_allow: ChatType = ChatType.ALL # 允许的聊天类型
|
||||
chatter_allow: list[str] = field(default_factory=list) # 允许的 Chatter 列表,空则允许所有
|
||||
# 二步Action相关属性
|
||||
is_two_step_action: bool = False # 是否为二步Action
|
||||
step_one_description: str = "" # 第一步的描述
|
||||
@@ -226,6 +227,8 @@ class ActionInfo(ComponentInfo):
|
||||
self.associated_types = []
|
||||
if self.sub_actions is None:
|
||||
self.sub_actions = []
|
||||
if self.chatter_allow is None:
|
||||
self.chatter_allow = []
|
||||
self.component_type = ComponentType.ACTION
|
||||
|
||||
|
||||
|
||||
@@ -258,7 +258,7 @@ class ChatterActionPlanner:
|
||||
# 3. 在规划前,先进行动作修改
|
||||
from src.chat.planner_actions.action_modifier import ActionModifier
|
||||
action_modifier = ActionModifier(self.action_manager, self.chat_id)
|
||||
await action_modifier.modify_actions()
|
||||
await action_modifier.modify_actions(chatter_name="AffinityFlowChatter")
|
||||
|
||||
# 4. 生成初始计划
|
||||
initial_plan = await self.generator.generate(ChatMode.FOCUS)
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
"""
|
||||
回复动作模块
|
||||
|
||||
定义了两种回复动作:
|
||||
定义了三种回复相关动作:
|
||||
- reply: 针对单条消息的深度回复(使用 s4u 模板)
|
||||
- respond: 对未读消息的统一回应(使用 normal 模板)
|
||||
- no_reply: 选择不回复
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import ClassVar
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import global_config
|
||||
from src.plugin_system import ActionActivationType, BaseAction, ChatMode
|
||||
from src.plugin_system.apis import database_api, generator_api, send_api
|
||||
|
||||
logger = get_logger("reply_actions")
|
||||
|
||||
@@ -21,6 +25,7 @@ class ReplyAction(BaseAction):
|
||||
- 使用 s4u (Speak for You) 模板
|
||||
- 专注于理解和回应单条消息的具体内容
|
||||
- 适合 Focus 模式下的精准回复
|
||||
- 仅限 AffinityFlowChatter 使用
|
||||
"""
|
||||
|
||||
# 动作基本信息
|
||||
@@ -31,6 +36,9 @@ class ReplyAction(BaseAction):
|
||||
activation_type = ActionActivationType.ALWAYS # 回复动作总是可用
|
||||
mode_enable = ChatMode.ALL # 在所有模式下都可用
|
||||
parallel_action = False # 回复动作不能与其他动作并行
|
||||
|
||||
# Chatter 限制:仅允许 AffinityFlowChatter 使用
|
||||
chatter_allow: ClassVar[list[str]] = ["AffinityFlowChatter"]
|
||||
|
||||
# 动作参数定义
|
||||
action_parameters: ClassVar = {
|
||||
@@ -53,13 +61,116 @@ class ReplyAction(BaseAction):
|
||||
associated_types: ClassVar[list[str]] = ["text"]
|
||||
|
||||
async def execute(self) -> tuple[bool, str]:
|
||||
"""执行reply动作
|
||||
"""执行reply动作 - 完整的回复流程"""
|
||||
try:
|
||||
# 检查目标消息是否为表情包
|
||||
if self.action_message and getattr(self.action_message, "is_emoji", False):
|
||||
if not getattr(global_config.chat, "allow_reply_to_emoji", True):
|
||||
logger.info(f"{self.log_prefix} 目标消息为表情包且配置不允许回复,跳过")
|
||||
return True, ""
|
||||
|
||||
# 准备 action_data
|
||||
action_data = self.action_data.copy()
|
||||
action_data["prompt_mode"] = "s4u"
|
||||
|
||||
# 生成回复
|
||||
success, response_set, _ = await generator_api.generate_reply(
|
||||
chat_stream=self.chat_stream,
|
||||
reply_message=self.action_message,
|
||||
action_data=action_data,
|
||||
available_actions={self.action_name: None},
|
||||
enable_tool=global_config.tool.enable_tool,
|
||||
request_type="chat.replyer",
|
||||
from_plugin=False,
|
||||
)
|
||||
|
||||
if not success or not response_set:
|
||||
logger.warning(f"{self.log_prefix} 回复生成失败")
|
||||
return False, ""
|
||||
|
||||
# 发送回复
|
||||
reply_text = await self._send_response(response_set)
|
||||
|
||||
# 存储动作信息
|
||||
await self._store_action_info(reply_text)
|
||||
|
||||
logger.info(f"{self.log_prefix} reply 动作执行成功")
|
||||
return True, reply_text
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.debug(f"{self.log_prefix} 回复任务被取消")
|
||||
return False, ""
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} reply 动作执行失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False, ""
|
||||
|
||||
async def _send_response(self, response_set) -> str:
|
||||
"""发送回复内容"""
|
||||
reply_text = ""
|
||||
should_quote = self.action_data.get("should_quote_reply", False)
|
||||
first_sent = False
|
||||
|
||||
注意:实际的回复生成由 action_manager 统一处理
|
||||
这里只是标记使用 reply 动作(s4u 模板)
|
||||
"""
|
||||
logger.info(f"{self.log_prefix} 使用 reply 动作(s4u 模板)")
|
||||
return True, ""
|
||||
for reply_seg in response_set:
|
||||
# 处理元组格式
|
||||
if isinstance(reply_seg, tuple) and len(reply_seg) >= 2:
|
||||
_, data = reply_seg
|
||||
else:
|
||||
data = str(reply_seg)
|
||||
|
||||
if isinstance(data, list):
|
||||
data = "".join(map(str, data))
|
||||
|
||||
reply_text += data
|
||||
|
||||
# 发送消息
|
||||
if not first_sent:
|
||||
await send_api.text_to_stream(
|
||||
text=data,
|
||||
stream_id=self.chat_stream.stream_id,
|
||||
reply_to_message=self.action_message,
|
||||
set_reply=should_quote and bool(self.action_message),
|
||||
typing=False,
|
||||
)
|
||||
first_sent = True
|
||||
else:
|
||||
await send_api.text_to_stream(
|
||||
text=data,
|
||||
stream_id=self.chat_stream.stream_id,
|
||||
reply_to_message=None,
|
||||
set_reply=False,
|
||||
typing=True,
|
||||
)
|
||||
|
||||
return reply_text
|
||||
|
||||
async def _store_action_info(self, reply_text: str):
|
||||
"""存储动作信息到数据库"""
|
||||
from src.person_info.person_info import get_person_info_manager
|
||||
|
||||
person_info_manager = get_person_info_manager()
|
||||
|
||||
if self.action_message:
|
||||
platform = self.action_message.chat_info.platform
|
||||
user_id = self.action_message.user_info.user_id
|
||||
else:
|
||||
platform = getattr(self.chat_stream, "platform", "unknown")
|
||||
user_id = ""
|
||||
|
||||
person_id = person_info_manager.get_person_id(platform, user_id)
|
||||
person_name = await person_info_manager.get_value(person_id, "person_name")
|
||||
action_prompt_display = f"你对{person_name}进行了回复:{reply_text}"
|
||||
|
||||
await database_api.store_action_info(
|
||||
chat_stream=self.chat_stream,
|
||||
action_build_into_prompt=False,
|
||||
action_prompt_display=action_prompt_display,
|
||||
action_done=True,
|
||||
thinking_id=self.thinking_id,
|
||||
action_data={"reply_text": reply_text},
|
||||
action_name="reply",
|
||||
)
|
||||
|
||||
|
||||
class RespondAction(BaseAction):
|
||||
@@ -69,6 +180,7 @@ class RespondAction(BaseAction):
|
||||
- 关注整体对话动态和未读消息的统一回应
|
||||
- 适合对于群聊消息下的宏观回应
|
||||
- 避免与单一用户深度对话而忽略其他用户的消息
|
||||
- 仅限 AffinityFlowChatter 使用
|
||||
"""
|
||||
|
||||
# 动作基本信息
|
||||
@@ -79,6 +191,9 @@ class RespondAction(BaseAction):
|
||||
activation_type = ActionActivationType.ALWAYS # 回应动作总是可用
|
||||
mode_enable = ChatMode.ALL # 在所有模式下都可用
|
||||
parallel_action = False # 回应动作不能与其他动作并行
|
||||
|
||||
# Chatter 限制:仅允许 AffinityFlowChatter 使用
|
||||
chatter_allow: ClassVar[list[str]] = ["AffinityFlowChatter"]
|
||||
|
||||
# 动作参数定义
|
||||
action_parameters: ClassVar = {
|
||||
@@ -99,10 +214,89 @@ class RespondAction(BaseAction):
|
||||
associated_types: ClassVar[list[str]] = ["text"]
|
||||
|
||||
async def execute(self) -> tuple[bool, str]:
|
||||
"""执行respond动作
|
||||
"""执行respond动作 - 完整的回复流程"""
|
||||
try:
|
||||
# 准备 action_data
|
||||
action_data = self.action_data.copy()
|
||||
action_data["prompt_mode"] = "normal"
|
||||
|
||||
# 生成回复
|
||||
success, response_set, _ = await generator_api.generate_reply(
|
||||
chat_stream=self.chat_stream,
|
||||
reply_message=self.action_message,
|
||||
action_data=action_data,
|
||||
available_actions={self.action_name: None},
|
||||
enable_tool=global_config.tool.enable_tool,
|
||||
request_type="chat.replyer",
|
||||
from_plugin=False,
|
||||
)
|
||||
|
||||
if not success or not response_set:
|
||||
logger.warning(f"{self.log_prefix} 回复生成失败")
|
||||
return False, ""
|
||||
|
||||
# 发送回复(respond 默认不引用)
|
||||
reply_text = await self._send_response(response_set)
|
||||
|
||||
# 存储动作信息
|
||||
await self._store_action_info(reply_text)
|
||||
|
||||
logger.info(f"{self.log_prefix} respond 动作执行成功")
|
||||
return True, reply_text
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.debug(f"{self.log_prefix} 回复任务被取消")
|
||||
return False, ""
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} respond 动作执行失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False, ""
|
||||
|
||||
async def _send_response(self, response_set) -> str:
|
||||
"""发送回复内容(不引用原消息)"""
|
||||
reply_text = ""
|
||||
first_sent = False
|
||||
|
||||
注意:实际的回复生成由 action_manager 统一处理
|
||||
这里只是标记使用 respond 动作(normal 模板)
|
||||
"""
|
||||
logger.info(f"{self.log_prefix} 使用 respond 动作(normal 模板)")
|
||||
return True, ""
|
||||
for reply_seg in response_set:
|
||||
if isinstance(reply_seg, tuple) and len(reply_seg) >= 2:
|
||||
_, data = reply_seg
|
||||
else:
|
||||
data = str(reply_seg)
|
||||
|
||||
if isinstance(data, list):
|
||||
data = "".join(map(str, data))
|
||||
|
||||
reply_text += data
|
||||
|
||||
if not first_sent:
|
||||
await send_api.text_to_stream(
|
||||
text=data,
|
||||
stream_id=self.chat_stream.stream_id,
|
||||
reply_to_message=None,
|
||||
set_reply=False,
|
||||
typing=False,
|
||||
)
|
||||
first_sent = True
|
||||
else:
|
||||
await send_api.text_to_stream(
|
||||
text=data,
|
||||
stream_id=self.chat_stream.stream_id,
|
||||
reply_to_message=None,
|
||||
set_reply=False,
|
||||
typing=True,
|
||||
)
|
||||
|
||||
return reply_text
|
||||
|
||||
async def _store_action_info(self, reply_text: str):
|
||||
"""存储动作信息到数据库"""
|
||||
await database_api.store_action_info(
|
||||
chat_stream=self.chat_stream,
|
||||
action_build_into_prompt=False,
|
||||
action_prompt_display=f"统一回应:{reply_text}",
|
||||
action_done=True,
|
||||
thinking_id=self.thinking_id,
|
||||
action_data={"reply_text": reply_text},
|
||||
action_name="respond",
|
||||
)
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
"""
|
||||
Kokoro Flow Chatter (心流聊天器) 插件
|
||||
|
||||
一个专为私聊场景设计的AI聊天插件,实现从"消息响应者"到"对话体验者"的转变。
|
||||
核心特点:
|
||||
- 心理状态驱动的交互模型
|
||||
- 连续的时间观念和等待体验
|
||||
- 深度情感连接和长期关系维护
|
||||
"""
|
||||
|
||||
from src.plugin_system.base.plugin_metadata import PluginMetadata
|
||||
|
||||
from .plugin import KokoroFlowChatterPlugin
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="Kokoro Flow Chatter",
|
||||
description="专为私聊设计的深度情感交互处理器,实现心理状态驱动的对话体验",
|
||||
usage="在私聊场景中自动启用,可通过 [kokoro_flow_chatter].enable 配置开关",
|
||||
version="3.0.0",
|
||||
author="MoFox",
|
||||
keywords=["chatter", "kokoro", "private", "emotional", "narrative"],
|
||||
categories=["Chat", "AI", "Emotional"],
|
||||
extra={"is_built_in": True, "chat_type": "private"},
|
||||
)
|
||||
|
||||
__all__ = ["KokoroFlowChatterPlugin", "__plugin_meta__"]
|
||||
@@ -1,711 +0,0 @@
|
||||
"""
|
||||
Kokoro Flow Chatter 动作执行器 (V2)
|
||||
|
||||
融合AFC的动态动作发现机制,支持所有注册的Action组件。
|
||||
负责解析LLM返回的动作列表并通过ChatterActionManager执行。
|
||||
|
||||
V2升级要点:
|
||||
1. 动态动作支持 - 使用ActionManager发现所有可用动作
|
||||
2. 统一执行接口 - 通过ChatterActionManager.execute_action()执行所有动作
|
||||
3. 保留KFC特有功能 - 内部状态更新、心理日志等
|
||||
4. 支持复合动作 - 如 sing_a_song + image_sender + tts_voice_action
|
||||
|
||||
V5升级要点:
|
||||
1. 动态情感更新 - 根据thought字段的情感倾向微调EmotionalState
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
import orjson
|
||||
|
||||
from src.chat.planner_actions.action_manager import ChatterActionManager
|
||||
from src.common.logger import get_logger
|
||||
from src.plugin_system.base.component_types import ActionInfo
|
||||
from src.utils.json_parser import extract_and_parse_json
|
||||
|
||||
from .models import (
|
||||
ActionModel,
|
||||
EmotionalState,
|
||||
KokoroSession,
|
||||
LLMResponseModel,
|
||||
MentalLogEntry,
|
||||
MentalLogEventType,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.chat.message_receive.chat_stream import ChatStream
|
||||
|
||||
logger = get_logger("kokoro_action_executor")
|
||||
|
||||
|
||||
class ActionExecutor:
|
||||
"""
|
||||
Kokoro Flow Chatter 动作执行器 (V2)
|
||||
|
||||
职责:
|
||||
1. 解析LLM返回的JSON响应
|
||||
2. 动态验证动作格式和参数(基于ActionManager的动作注册)
|
||||
3. 通过ChatterActionManager执行各类动作
|
||||
4. 处理KFC特有的内部状态更新
|
||||
5. 记录执行结果到心理日志
|
||||
|
||||
V2特性:
|
||||
- 支持所有通过插件系统注册的Action
|
||||
- 自动从ActionManager获取可用动作列表
|
||||
- 支持复合动作组合执行
|
||||
- 区分"回复类动作"和"其他动作"的执行顺序
|
||||
"""
|
||||
|
||||
# KFC内置的特殊动作(不通过ActionManager执行)
|
||||
INTERNAL_ACTIONS = {
|
||||
"update_internal_state": {
|
||||
"required": [],
|
||||
"optional": ["mood", "mood_intensity", "relationship_warmth", "impression_of_user", "anxiety_level", "engagement_level"]
|
||||
},
|
||||
"do_nothing": {"required": [], "optional": []},
|
||||
}
|
||||
|
||||
def __init__(self, stream_id: str):
|
||||
"""
|
||||
初始化动作执行器
|
||||
|
||||
Args:
|
||||
stream_id: 聊天流ID
|
||||
"""
|
||||
self.stream_id = stream_id
|
||||
self._action_manager = ChatterActionManager()
|
||||
self._available_actions: dict[str, ActionInfo] = {}
|
||||
self._execution_stats = {
|
||||
"total_executed": 0,
|
||||
"successful": 0,
|
||||
"failed": 0,
|
||||
"by_type": {},
|
||||
}
|
||||
|
||||
async def load_actions(self) -> dict[str, ActionInfo]:
|
||||
"""
|
||||
加载当前可用的动作列表
|
||||
|
||||
Returns:
|
||||
dict[str, ActionInfo]: 可用动作字典
|
||||
"""
|
||||
await self._action_manager.load_actions(self.stream_id)
|
||||
self._available_actions = self._action_manager.get_using_actions()
|
||||
logger.debug(f"KFC ActionExecutor 加载了 {len(self._available_actions)} 个可用动作: {list(self._available_actions.keys())}")
|
||||
return self._available_actions
|
||||
|
||||
def get_available_actions(self) -> dict[str, ActionInfo]:
|
||||
"""获取当前可用的动作列表"""
|
||||
return self._available_actions.copy()
|
||||
|
||||
def is_action_available(self, action_type: str) -> bool:
|
||||
"""
|
||||
检查动作是否可用
|
||||
|
||||
Args:
|
||||
action_type: 动作类型名称
|
||||
|
||||
Returns:
|
||||
bool: 动作是否可用
|
||||
"""
|
||||
# 内置动作总是可用
|
||||
if action_type in self.INTERNAL_ACTIONS:
|
||||
return True
|
||||
# 检查动态注册的动作
|
||||
return action_type in self._available_actions
|
||||
|
||||
def parse_llm_response(self, response_text: str) -> LLMResponseModel:
|
||||
"""
|
||||
解析LLM的JSON响应
|
||||
|
||||
使用统一的json_parser工具进行解析,自动处理:
|
||||
- Markdown代码块标记
|
||||
- 格式错误的JSON修复(json_repair)
|
||||
- 多种包装格式
|
||||
|
||||
Args:
|
||||
response_text: LLM返回的原始文本
|
||||
|
||||
Returns:
|
||||
LLMResponseModel: 解析后的响应模型
|
||||
"""
|
||||
# 使用统一的json_parser工具解析
|
||||
data = extract_and_parse_json(response_text, strict=False)
|
||||
|
||||
if not data or not isinstance(data, dict):
|
||||
logger.warning(f"无法从LLM响应中提取有效JSON: {response_text[:200]}...")
|
||||
return LLMResponseModel.create_error_response("无法解析响应格式")
|
||||
|
||||
return self._validate_and_create_response(data)
|
||||
|
||||
def _validate_and_create_response(self, data: dict[str, Any]) -> LLMResponseModel:
|
||||
"""
|
||||
验证并创建响应模型(V2:支持动态动作验证)
|
||||
|
||||
Args:
|
||||
data: 解析后的字典数据
|
||||
|
||||
Returns:
|
||||
LLMResponseModel: 验证后的响应模型
|
||||
"""
|
||||
# 验证必需字段
|
||||
if "thought" not in data:
|
||||
data["thought"] = ""
|
||||
logger.warning("LLM响应缺少'thought'字段")
|
||||
|
||||
if "expected_user_reaction" not in data:
|
||||
data["expected_user_reaction"] = ""
|
||||
logger.warning("LLM响应缺少'expected_user_reaction'字段")
|
||||
|
||||
if "max_wait_seconds" not in data:
|
||||
data["max_wait_seconds"] = 300
|
||||
logger.warning("LLM响应缺少'max_wait_seconds'字段,使用默认值300")
|
||||
else:
|
||||
# 确保在合理范围内:0-900秒
|
||||
# 0 表示不等待(话题结束/用户说再见等)
|
||||
try:
|
||||
wait_seconds = int(data["max_wait_seconds"])
|
||||
data["max_wait_seconds"] = max(0, min(wait_seconds, 900))
|
||||
except (ValueError, TypeError):
|
||||
data["max_wait_seconds"] = 300
|
||||
|
||||
if "actions" not in data or not data["actions"]:
|
||||
data["actions"] = [{"type": "do_nothing"}]
|
||||
logger.warning("LLM响应缺少'actions'字段,添加默认的do_nothing动作")
|
||||
|
||||
# 验证每个动作(V2:使用动态验证)
|
||||
validated_actions = []
|
||||
for action_data in data["actions"]:
|
||||
if not isinstance(action_data, dict):
|
||||
logger.warning(f"无效的动作格式: {action_data}")
|
||||
continue
|
||||
|
||||
action_type = action_data.get("type", "")
|
||||
|
||||
# 检查是否是已注册的动作
|
||||
if not self.is_action_available(action_type):
|
||||
logger.warning(f"不支持的动作类型: {action_type},可用动作: {list(self._available_actions.keys()) + list(self.INTERNAL_ACTIONS.keys())}")
|
||||
continue
|
||||
|
||||
# 对于内置动作,验证参数
|
||||
if action_type in self.INTERNAL_ACTIONS:
|
||||
required_params = self.INTERNAL_ACTIONS[action_type]["required"]
|
||||
missing_params = [p for p in required_params if p not in action_data]
|
||||
if missing_params:
|
||||
logger.warning(f"动作 '{action_type}' 缺少必需参数: {missing_params}")
|
||||
continue
|
||||
|
||||
# 对于动态注册的动作,仅记录参数信息(不强制验证)
|
||||
# 注意:action_require 是"使用场景描述",不是必需参数!
|
||||
# 必需参数应该在 action_parameters 中定义
|
||||
elif action_type in self._available_actions:
|
||||
action_info = self._available_actions[action_type]
|
||||
# 仅记录调试信息,不阻止执行
|
||||
if action_info.action_parameters:
|
||||
provided_params = set(action_data.keys()) - {"type", "reason"}
|
||||
expected_params = set(action_info.action_parameters.keys())
|
||||
if expected_params and not provided_params.intersection(expected_params):
|
||||
logger.debug(f"动作 '{action_type}' 期望参数: {list(expected_params)},实际提供: {list(provided_params)}")
|
||||
|
||||
validated_actions.append(action_data)
|
||||
|
||||
if not validated_actions:
|
||||
validated_actions = [{"type": "do_nothing"}]
|
||||
|
||||
data["actions"] = validated_actions
|
||||
|
||||
return LLMResponseModel.from_dict(data)
|
||||
|
||||
async def execute_actions(
|
||||
self,
|
||||
response: LLMResponseModel,
|
||||
session: KokoroSession,
|
||||
chat_stream: Optional["ChatStream"] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
执行动作列表(V2:通过ActionManager执行动态动作)
|
||||
|
||||
执行策略(参考AFC的plan_executor):
|
||||
1. 先执行所有"回复类"动作(reply, respond等)
|
||||
2. 再执行"其他"动作(send_reaction, sing_a_song等)
|
||||
3. 内部动作(update_internal_state, do_nothing)由KFC直接处理
|
||||
|
||||
Args:
|
||||
response: LLM响应模型
|
||||
session: 当前会话
|
||||
chat_stream: 聊天流对象(用于发送消息)
|
||||
|
||||
Returns:
|
||||
dict: 执行结果
|
||||
"""
|
||||
results = []
|
||||
has_reply = False
|
||||
reply_content = ""
|
||||
|
||||
# INFO日志:打印所有解析出的动作(可观测性增强)
|
||||
for action in response.actions:
|
||||
logger.info(
|
||||
f"Parsed action for execution: type={action.type}, params={action.params}"
|
||||
)
|
||||
|
||||
# 分类动作:回复类 vs 其他类 vs 内部类
|
||||
reply_actions = [] # reply, respond
|
||||
other_actions = [] # 其他注册的动作
|
||||
internal_actions = [] # update_internal_state, do_nothing
|
||||
|
||||
for action in response.actions:
|
||||
action_type = action.type
|
||||
if action_type in self.INTERNAL_ACTIONS:
|
||||
internal_actions.append(action)
|
||||
elif action_type in ("reply", "respond"):
|
||||
reply_actions.append(action)
|
||||
else:
|
||||
other_actions.append(action)
|
||||
|
||||
# 第1步:执行回复类动作
|
||||
for action in reply_actions:
|
||||
try:
|
||||
result = await self._execute_via_action_manager(
|
||||
action, session, chat_stream
|
||||
)
|
||||
results.append(result)
|
||||
|
||||
if result.get("success"):
|
||||
self._execution_stats["successful"] += 1
|
||||
has_reply = True
|
||||
reply_content = action.params.get("content", "") or result.get("reply_text", "")
|
||||
else:
|
||||
self._execution_stats["failed"] += 1
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"执行回复动作 '{action.type}' 失败: {e}")
|
||||
results.append({
|
||||
"action_type": action.type,
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
})
|
||||
self._execution_stats["failed"] += 1
|
||||
|
||||
self._update_stats(action.type)
|
||||
|
||||
# 第2步:并行执行其他动作(参考AFC的_execute_other_actions)
|
||||
if other_actions:
|
||||
other_tasks = []
|
||||
for action in other_actions:
|
||||
task = asyncio.create_task(
|
||||
self._execute_via_action_manager(action, session, chat_stream)
|
||||
)
|
||||
other_tasks.append((action, task))
|
||||
|
||||
for action, task in other_tasks:
|
||||
try:
|
||||
result = await task
|
||||
results.append(result)
|
||||
if result.get("success"):
|
||||
self._execution_stats["successful"] += 1
|
||||
else:
|
||||
self._execution_stats["failed"] += 1
|
||||
except Exception as e:
|
||||
logger.error(f"执行动作 '{action.type}' 失败: {e}")
|
||||
results.append({
|
||||
"action_type": action.type,
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
})
|
||||
self._execution_stats["failed"] += 1
|
||||
|
||||
self._update_stats(action.type)
|
||||
|
||||
# 第3步:执行内部动作
|
||||
for action in internal_actions:
|
||||
try:
|
||||
result = await self._execute_internal_action(action, session)
|
||||
results.append(result)
|
||||
self._execution_stats["successful"] += 1
|
||||
except Exception as e:
|
||||
logger.error(f"执行内部动作 '{action.type}' 失败: {e}")
|
||||
results.append({
|
||||
"action_type": action.type,
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
})
|
||||
self._execution_stats["failed"] += 1
|
||||
|
||||
self._update_stats(action.type)
|
||||
|
||||
# 添加Bot行动日志
|
||||
if has_reply or other_actions:
|
||||
entry = MentalLogEntry(
|
||||
event_type=MentalLogEventType.BOT_ACTION,
|
||||
timestamp=time.time(),
|
||||
thought=response.thought,
|
||||
content=reply_content or f"执行了 {len(other_actions)} 个动作",
|
||||
emotional_snapshot=session.emotional_state.to_dict(),
|
||||
metadata={
|
||||
"actions": [a.to_dict() for a in response.actions],
|
||||
"results_summary": {
|
||||
"total": len(results),
|
||||
"successful": sum(1 for r in results if r.get("success")),
|
||||
},
|
||||
},
|
||||
)
|
||||
session.add_mental_log_entry(entry)
|
||||
if reply_content:
|
||||
session.last_bot_message = reply_content
|
||||
|
||||
# V5:动态情感更新 - 根据thought分析情感倾向并微调EmotionalState
|
||||
await self._update_emotional_state_from_thought(response.thought, session)
|
||||
|
||||
return {
|
||||
"success": all(r.get("success", False) for r in results),
|
||||
"results": results,
|
||||
"has_reply": has_reply,
|
||||
"reply_content": reply_content,
|
||||
"thought": response.thought,
|
||||
"expected_user_reaction": response.expected_user_reaction,
|
||||
"max_wait_seconds": response.max_wait_seconds,
|
||||
}
|
||||
|
||||
def _update_stats(self, action_type: str) -> None:
|
||||
"""更新执行统计"""
|
||||
self._execution_stats["total_executed"] += 1
|
||||
if action_type not in self._execution_stats["by_type"]:
|
||||
self._execution_stats["by_type"][action_type] = 0
|
||||
self._execution_stats["by_type"][action_type] += 1
|
||||
|
||||
async def _execute_via_action_manager(
|
||||
self,
|
||||
action: ActionModel,
|
||||
session: KokoroSession,
|
||||
chat_stream: Optional["ChatStream"],
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
通过ActionManager执行动作
|
||||
|
||||
Args:
|
||||
action: 动作模型
|
||||
session: 当前会话
|
||||
chat_stream: 聊天流对象
|
||||
|
||||
Returns:
|
||||
dict: 执行结果
|
||||
"""
|
||||
action_type = action.type
|
||||
params = action.params
|
||||
|
||||
logger.debug(f"通过ActionManager执行动作: {action_type}, 参数: {params}")
|
||||
|
||||
if not chat_stream:
|
||||
return {
|
||||
"action_type": action_type,
|
||||
"success": False,
|
||||
"error": "无法获取聊天流",
|
||||
}
|
||||
|
||||
try:
|
||||
# 准备动作数据
|
||||
action_data = params.copy()
|
||||
|
||||
# 对于reply动作,需要处理content字段
|
||||
if action_type in ("reply", "respond") and "content" in action_data:
|
||||
# ActionManager的reply期望的是生成回复而不是直接内容
|
||||
# 但KFC已经决定了内容,所以我们直接发送
|
||||
return await self._execute_reply_directly(action_data, chat_stream)
|
||||
|
||||
# 使用ActionManager执行其他动作
|
||||
result = await self._action_manager.execute_action(
|
||||
action_name=action_type,
|
||||
chat_id=self.stream_id,
|
||||
target_message=None, # KFC模式不需要target_message
|
||||
reasoning=f"KFC决策: {action_type}",
|
||||
action_data=action_data,
|
||||
thinking_id=None,
|
||||
log_prefix="[KFC]",
|
||||
)
|
||||
|
||||
return {
|
||||
"action_type": action_type,
|
||||
"success": result.get("success", False),
|
||||
"reply_text": result.get("reply_text", ""),
|
||||
"result": result,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"ActionManager执行失败: {action_type}, 错误: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
return {
|
||||
"action_type": action_type,
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
async def _execute_reply_directly(
|
||||
self,
|
||||
params: dict[str, Any],
|
||||
chat_stream: "ChatStream",
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
直接执行回复动作(KFC决定的内容直接发送)
|
||||
|
||||
V4升级:集成全局后处理流程(错别字、消息分割)
|
||||
|
||||
Args:
|
||||
params: 动作参数,包含content
|
||||
chat_stream: 聊天流对象
|
||||
|
||||
Returns:
|
||||
dict: 执行结果
|
||||
"""
|
||||
from src.plugin_system.apis import send_api
|
||||
from .response_post_processor import process_reply_content
|
||||
|
||||
content = params.get("content", "")
|
||||
reply_to = params.get("reply_to")
|
||||
should_quote = params.get("should_quote_reply", False)
|
||||
|
||||
if not content:
|
||||
return {
|
||||
"action_type": "reply",
|
||||
"success": False,
|
||||
"error": "回复内容为空",
|
||||
}
|
||||
|
||||
try:
|
||||
# 【关键步骤】调用全局后处理器(错别字生成、消息分割)
|
||||
processed_messages = await process_reply_content(content)
|
||||
logger.info(f"[KFC] 后处理完成,原始内容长度={len(content)},分割为 {len(processed_messages)} 条消息")
|
||||
|
||||
all_success = True
|
||||
first_message = True
|
||||
|
||||
for msg in processed_messages:
|
||||
success = await send_api.text_to_stream(
|
||||
text=msg,
|
||||
stream_id=self.stream_id,
|
||||
reply_to_message=reply_to if first_message else None,
|
||||
set_reply=should_quote if first_message else False,
|
||||
typing=True,
|
||||
)
|
||||
if not success:
|
||||
all_success = False
|
||||
first_message = False
|
||||
|
||||
return {
|
||||
"action_type": "reply",
|
||||
"success": all_success,
|
||||
"reply_text": content,
|
||||
"processed_messages": processed_messages,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"直接发送回复失败: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
return {
|
||||
"action_type": "reply",
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
async def _execute_internal_action(
|
||||
self,
|
||||
action: ActionModel,
|
||||
session: KokoroSession,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
执行KFC内部动作
|
||||
|
||||
Args:
|
||||
action: 动作模型
|
||||
session: 当前会话
|
||||
|
||||
Returns:
|
||||
dict: 执行结果
|
||||
"""
|
||||
action_type = action.type
|
||||
params = action.params
|
||||
|
||||
if action_type == "update_internal_state":
|
||||
return await self._execute_update_state(params, session)
|
||||
|
||||
elif action_type == "do_nothing":
|
||||
return await self._execute_do_nothing()
|
||||
|
||||
else:
|
||||
return {
|
||||
"action_type": action_type,
|
||||
"success": False,
|
||||
"error": f"未知的内部动作类型: {action_type}",
|
||||
}
|
||||
|
||||
async def _execute_update_state(
|
||||
self,
|
||||
params: dict[str, Any],
|
||||
session: KokoroSession,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
执行内部状态更新动作
|
||||
|
||||
V7重构:情绪变化必须合理
|
||||
- 禁止 LLM 直接设置负面情绪(低落、沮丧、难过等)
|
||||
- 情绪变化必须渐进,不能突然跳变
|
||||
- 情绪强度变化限制在 ±0.3 以内
|
||||
"""
|
||||
updated_fields = []
|
||||
emotional_state = session.emotional_state
|
||||
blocked_fields = []
|
||||
|
||||
if "mood" in params:
|
||||
new_mood = str(params["mood"])
|
||||
# V7: 检查是否是负面情绪
|
||||
negative_moods = [
|
||||
"低落", "沮丧", "难过", "伤心", "失落", "郁闷", "烦躁", "焦虑",
|
||||
"担忧", "害怕", "恐惧", "愤怒", "生气", "不安", "忧郁", "悲伤",
|
||||
"sad", "depressed", "anxious", "angry", "upset", "worried"
|
||||
]
|
||||
is_negative = any(neg in new_mood.lower() for neg in negative_moods)
|
||||
|
||||
if is_negative:
|
||||
# 负面情绪需要检查是否有合理理由(通过检查上下文)
|
||||
# 如果当前情绪是平静/正面的,不允许突然变成负面
|
||||
current_mood = emotional_state.mood.lower()
|
||||
positive_indicators = ["平静", "开心", "愉快", "高兴", "满足", "期待", "好奇", "neutral"]
|
||||
|
||||
if any(pos in current_mood for pos in positive_indicators):
|
||||
# 从正面情绪直接跳到负面情绪,阻止这种变化
|
||||
logger.warning(
|
||||
f"[KFC] 阻止无厘头负面情绪变化: {emotional_state.mood} -> {new_mood},"
|
||||
f"情绪变化必须有聊天上下文支撑"
|
||||
)
|
||||
blocked_fields.append("mood")
|
||||
else:
|
||||
# 已经是非正面情绪,允许变化但记录警告
|
||||
emotional_state.mood = new_mood
|
||||
updated_fields.append("mood")
|
||||
logger.info(f"[KFC] 情绪变化: {emotional_state.mood} -> {new_mood}")
|
||||
else:
|
||||
# 非负面情绪,允许更新
|
||||
emotional_state.mood = new_mood
|
||||
updated_fields.append("mood")
|
||||
|
||||
if "mood_intensity" in params:
|
||||
try:
|
||||
new_intensity = float(params["mood_intensity"])
|
||||
new_intensity = max(0.0, min(1.0, new_intensity))
|
||||
old_intensity = emotional_state.mood_intensity
|
||||
|
||||
# V7: 限制情绪强度变化幅度(最多 ±0.3)
|
||||
max_change = 0.3
|
||||
if abs(new_intensity - old_intensity) > max_change:
|
||||
# 限制变化幅度
|
||||
if new_intensity > old_intensity:
|
||||
new_intensity = min(old_intensity + max_change, 1.0)
|
||||
else:
|
||||
new_intensity = max(old_intensity - max_change, 0.0)
|
||||
logger.info(
|
||||
f"[KFC] 限制情绪强度变化: {old_intensity:.2f} -> {new_intensity:.2f} "
|
||||
f"(原请求: {params['mood_intensity']})"
|
||||
)
|
||||
|
||||
emotional_state.mood_intensity = new_intensity
|
||||
updated_fields.append("mood_intensity")
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# relationship_warmth 不再由 LLM 更新,应该从全局关系系统读取
|
||||
if "relationship_warmth" in params:
|
||||
logger.debug("[KFC] 忽略 relationship_warmth 更新,应从全局关系系统读取")
|
||||
blocked_fields.append("relationship_warmth")
|
||||
|
||||
if "impression_of_user" in params:
|
||||
emotional_state.impression_of_user = str(params["impression_of_user"])
|
||||
updated_fields.append("impression_of_user")
|
||||
|
||||
if "anxiety_level" in params:
|
||||
try:
|
||||
anxiety = float(params["anxiety_level"])
|
||||
emotional_state.anxiety_level = max(0.0, min(1.0, anxiety))
|
||||
updated_fields.append("anxiety_level")
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
if "engagement_level" in params:
|
||||
try:
|
||||
engagement = float(params["engagement_level"])
|
||||
emotional_state.engagement_level = max(0.0, min(1.0, engagement))
|
||||
updated_fields.append("engagement_level")
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
emotional_state.last_update_time = time.time()
|
||||
|
||||
if blocked_fields:
|
||||
logger.debug(f"更新情感状态: 更新={updated_fields}, 阻止={blocked_fields}")
|
||||
else:
|
||||
logger.debug(f"更新情感状态: {updated_fields}")
|
||||
|
||||
return {
|
||||
"action_type": "update_internal_state",
|
||||
"success": True,
|
||||
"updated_fields": updated_fields,
|
||||
"blocked_fields": blocked_fields,
|
||||
}
|
||||
|
||||
async def _execute_do_nothing(self) -> dict[str, Any]:
|
||||
"""执行"什么都不做"动作"""
|
||||
logger.debug("执行 do_nothing 动作")
|
||||
return {
|
||||
"action_type": "do_nothing",
|
||||
"success": True,
|
||||
}
|
||||
|
||||
def get_execution_stats(self) -> dict[str, Any]:
|
||||
"""获取执行统计信息"""
|
||||
return self._execution_stats.copy()
|
||||
|
||||
def reset_stats(self) -> None:
|
||||
"""重置统计信息"""
|
||||
self._execution_stats = {
|
||||
"total_executed": 0,
|
||||
"successful": 0,
|
||||
"failed": 0,
|
||||
"by_type": {},
|
||||
}
|
||||
|
||||
async def _update_emotional_state_from_thought(
|
||||
self,
|
||||
thought: str,
|
||||
session: KokoroSession,
|
||||
) -> None:
|
||||
"""
|
||||
根据thought字段更新EmotionalState
|
||||
|
||||
V6重构:
|
||||
- 移除基于关键词的情感分析(诡异且不准确)
|
||||
- 情感状态现在主要通过LLM输出的update_internal_state动作更新
|
||||
- 关系温度应该从person_info/relationship_manager的好感度系统读取
|
||||
- 此方法仅做简单的engagement_level更新
|
||||
|
||||
Args:
|
||||
thought: LLM返回的内心独白
|
||||
session: 当前会话
|
||||
"""
|
||||
if not thought:
|
||||
return
|
||||
|
||||
emotional_state = session.emotional_state
|
||||
|
||||
# 简单的engagement_level更新:有内容的thought表示高投入
|
||||
if len(thought) > 50:
|
||||
old_engagement = emotional_state.engagement_level
|
||||
new_engagement = old_engagement + 0.025 # 微调
|
||||
emotional_state.engagement_level = max(0.0, min(1.0, new_engagement))
|
||||
|
||||
emotional_state.last_update_time = time.time()
|
||||
|
||||
# 注意:关系温度(relationship_warmth)应该从全局的好感度系统读取
|
||||
# 参考 src/person_info/relationship_manager.py 和 src/plugin_system/apis/person_api.py
|
||||
# 当前实现中,这个值主要通过 LLM 的 update_internal_state 动作来更新
|
||||
@@ -1,877 +0,0 @@
|
||||
"""
|
||||
Kokoro Flow Chatter (心流聊天器) 主类
|
||||
|
||||
核心聊天处理器,协调所有组件完成"体验-决策-行动"的交互循环。
|
||||
实现从"消息响应者"到"对话体验者"的核心转变。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
import traceback
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Optional
|
||||
|
||||
from src.chat.planner_actions.action_manager import ChatterActionManager
|
||||
from src.chat.planner_actions.action_modifier import ActionModifier # V6: 动作筛选器
|
||||
from src.common.data_models.message_manager_data_model import StreamContext
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import global_config, model_config
|
||||
from src.llm_models.utils_model import LLMRequest
|
||||
from src.plugin_system.base.base_chatter import BaseChatter
|
||||
from src.plugin_system.base.component_types import ChatType
|
||||
|
||||
from .action_executor import ActionExecutor
|
||||
from .context_builder import KFCContextBuilder
|
||||
from .models import (
|
||||
KokoroSession,
|
||||
LLMResponseModel,
|
||||
MentalLogEntry,
|
||||
MentalLogEventType,
|
||||
SessionStatus,
|
||||
)
|
||||
from .prompt_generator import PromptGenerator, get_prompt_generator
|
||||
from .kfc_scheduler_adapter import KFCSchedulerAdapter, get_scheduler
|
||||
from .session_manager import SessionManager, get_session_manager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.common.data_models.database_data_model import DatabaseMessages
|
||||
|
||||
logger = get_logger("kokoro_flow_chatter")
|
||||
|
||||
# 控制台颜色
|
||||
SOFT_PURPLE = "\033[38;5;183m"
|
||||
RESET_COLOR = "\033[0m"
|
||||
|
||||
|
||||
class KokoroFlowChatter(BaseChatter):
|
||||
"""
|
||||
心流聊天器 (Kokoro Flow Chatter)
|
||||
|
||||
专为私聊场景设计的AI聊天处理器,核心特点:
|
||||
- 心理状态驱动的交互模型
|
||||
- 连续的时间观念和等待体验
|
||||
- 深度情感连接和长期关系维护
|
||||
|
||||
状态机:
|
||||
IDLE -> RESPONDING -> WAITING -> (收到消息) -> RESPONDING
|
||||
-> (超时) -> FOLLOW_UP_PENDING -> RESPONDING/IDLE
|
||||
"""
|
||||
|
||||
chatter_name: str = "KokoroFlowChatter"
|
||||
chatter_description: str = "心流聊天器 - 专为私聊设计的深度情感交互处理器"
|
||||
chat_types: ClassVar[list[ChatType]] = [ChatType.PRIVATE] # 仅支持私聊
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
stream_id: str,
|
||||
action_manager: ChatterActionManager,
|
||||
plugin_config: dict | None = None,
|
||||
):
|
||||
"""
|
||||
初始化心流聊天器
|
||||
|
||||
Args:
|
||||
stream_id: 聊天流ID
|
||||
action_manager: 动作管理器
|
||||
plugin_config: 插件配置
|
||||
"""
|
||||
super().__init__(stream_id, action_manager, plugin_config)
|
||||
|
||||
# 核心组件
|
||||
self.session_manager: SessionManager = get_session_manager()
|
||||
self.prompt_generator: PromptGenerator = get_prompt_generator()
|
||||
self.scheduler: KFCSchedulerAdapter = get_scheduler()
|
||||
self.action_executor: ActionExecutor = ActionExecutor(stream_id)
|
||||
|
||||
# 配置
|
||||
self._load_config()
|
||||
|
||||
# 并发控制
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
# V7: 打断机制(类似S4U的已读/未读,这里是已处理/未处理)
|
||||
self._current_task: Optional[asyncio.Task] = None # 当前正在执行的任务
|
||||
self._interrupt_requested: bool = False # 是否请求打断
|
||||
self._interrupt_wait_seconds: float = 3.0 # 被打断后等待新消息的时间
|
||||
self._last_interrupt_time: float = 0.0 # 上次被打断的时间
|
||||
self._pending_message_ids: set[str] = set() # 未处理的消息ID集合(被打断时保留)
|
||||
self._current_processing_message_id: Optional[str] = None # 当前正在处理的消息ID
|
||||
|
||||
# 统计信息
|
||||
self.stats = {
|
||||
"messages_processed": 0,
|
||||
"llm_calls": 0,
|
||||
"successful_responses": 0,
|
||||
"failed_responses": 0,
|
||||
"timeout_decisions": 0,
|
||||
"interrupts": 0, # V7: 打断次数统计
|
||||
}
|
||||
self.last_activity_time = time.time()
|
||||
|
||||
# 设置调度器回调
|
||||
self._setup_scheduler_callbacks()
|
||||
|
||||
logger.info(f"{SOFT_PURPLE}[KFC]{RESET_COLOR} 初始化完成: stream_id={stream_id}")
|
||||
|
||||
def _load_config(self) -> None:
|
||||
"""
|
||||
加载配置(从 global_config.kokoro_flow_chatter 读取)
|
||||
|
||||
设计理念:KFC不是独立人格,它复用全局的人设、情感框架和回复模型,
|
||||
只保留最少的行为控制开关。
|
||||
"""
|
||||
# 获取 KFC 配置
|
||||
if global_config and hasattr(global_config, 'kokoro_flow_chatter'):
|
||||
kfc_config = global_config.kokoro_flow_chatter
|
||||
|
||||
# 核心行为配置
|
||||
self.max_wait_seconds_default: int = kfc_config.max_wait_seconds_default
|
||||
self.enable_continuous_thinking: bool = kfc_config.enable_continuous_thinking
|
||||
|
||||
# 主动思考子配置(V3: 人性化驱动,无机械限制)
|
||||
proactive_cfg = kfc_config.proactive_thinking
|
||||
self.enable_proactive: bool = proactive_cfg.enabled
|
||||
self.silence_threshold_seconds: int = proactive_cfg.silence_threshold_seconds
|
||||
self.min_interval_between_proactive: int = proactive_cfg.min_interval_between_proactive
|
||||
|
||||
logger.debug("[KFC] 已从 global_config.kokoro_flow_chatter 加载配置")
|
||||
else:
|
||||
# 回退到默认值
|
||||
self.max_wait_seconds_default = 300
|
||||
self.enable_continuous_thinking = True
|
||||
self.enable_proactive = True
|
||||
self.silence_threshold_seconds = 7200
|
||||
self.min_interval_between_proactive = 1800
|
||||
|
||||
logger.debug("[KFC] 使用默认配置")
|
||||
|
||||
def _setup_scheduler_callbacks(self) -> None:
|
||||
"""设置调度器回调"""
|
||||
self.scheduler.set_timeout_callback(self._on_session_timeout)
|
||||
|
||||
if self.enable_continuous_thinking:
|
||||
self.scheduler.set_continuous_thinking_callback(
|
||||
self._on_continuous_thinking
|
||||
)
|
||||
|
||||
# 设置主动思考回调
|
||||
if self.enable_proactive:
|
||||
self.scheduler.set_proactive_thinking_callback(
|
||||
self._on_proactive_thinking
|
||||
)
|
||||
|
||||
async def execute(self, context: StreamContext) -> dict:
|
||||
"""
|
||||
执行聊天处理逻辑(BaseChatter接口实现)
|
||||
|
||||
V7升级:实现打断机制(类似S4U的已读/未读机制)
|
||||
- 如果当前有任务在执行,新消息会请求打断
|
||||
- 被打断时,当前处理的消息会被标记为"未处理"(pending)
|
||||
- 下次处理时,会合并所有pending消息 + 新消息一起处理
|
||||
- 这样被打断的消息不会丢失,上下文关联性得以保持
|
||||
|
||||
Args:
|
||||
context: StreamContext对象,包含聊天上下文信息
|
||||
|
||||
Returns:
|
||||
处理结果字典
|
||||
"""
|
||||
# V7: 检查是否需要打断当前任务
|
||||
if self._current_task and not self._current_task.done():
|
||||
logger.info(f"[KFC] 收到新消息,请求打断当前任务: {self.stream_id}")
|
||||
self._interrupt_requested = True
|
||||
self.stats["interrupts"] += 1
|
||||
|
||||
# 返回一个特殊结果表示请求打断
|
||||
# 注意:当前正在处理的消息会在被打断时自动加入 pending 列表
|
||||
return self._build_result(
|
||||
success=True,
|
||||
message="interrupt_requested",
|
||||
interrupted=True
|
||||
)
|
||||
|
||||
# V7: 检查是否需要等待(刚被打断过,等待用户可能的连续输入)
|
||||
time_since_interrupt = time.time() - self._last_interrupt_time
|
||||
if time_since_interrupt < self._interrupt_wait_seconds and self._last_interrupt_time > 0:
|
||||
wait_remaining = self._interrupt_wait_seconds - time_since_interrupt
|
||||
logger.info(f"[KFC] 刚被打断,等待 {wait_remaining:.1f}s 收集更多消息: {self.stream_id}")
|
||||
await asyncio.sleep(wait_remaining)
|
||||
|
||||
async with self._lock:
|
||||
try:
|
||||
self.last_activity_time = time.time()
|
||||
self._interrupt_requested = False
|
||||
|
||||
# 创建任务以便可以被打断
|
||||
self._current_task = asyncio.current_task()
|
||||
|
||||
# V7: 获取所有未读消息
|
||||
# 注意:被打断的消息不会被标记为已读,所以仍然在 unread 列表中
|
||||
unread_messages = context.get_unread_messages()
|
||||
|
||||
if not unread_messages:
|
||||
logger.debug(f"[KFC] 没有未读消息: {self.stream_id}")
|
||||
return self._build_result(success=True, message="no_unread_messages")
|
||||
|
||||
# V7: 记录是否有 pending 消息(被打断时遗留的)
|
||||
pending_count = len(self._pending_message_ids)
|
||||
if pending_count > 0:
|
||||
# 日志:显示有多少消息是被打断后重新处理的
|
||||
new_count = sum(1 for msg in unread_messages
|
||||
if str(msg.message_id) not in self._pending_message_ids)
|
||||
logger.info(
|
||||
f"[KFC] 打断恢复: 正在处理 {len(unread_messages)} 条消息 "
|
||||
f"({pending_count} 条pending + {new_count} 条新消息): {self.stream_id}"
|
||||
)
|
||||
|
||||
# 以最后一条消息为主消息(用于动作筛选和主要响应)
|
||||
target_message = unread_messages[-1]
|
||||
|
||||
# 记录当前正在处理的消息ID(用于被打断时标记为pending)
|
||||
self._current_processing_message_id = str(target_message.message_id)
|
||||
|
||||
message_content = self._extract_message_content(target_message)
|
||||
|
||||
# V2: 加载可用动作(动态动作发现)
|
||||
await self.action_executor.load_actions()
|
||||
raw_action_count = len(self.action_executor.get_available_actions())
|
||||
logger.debug(f"[KFC] 原始加载 {raw_action_count} 个动作")
|
||||
|
||||
# V7: 在动作筛选前检查是否被打断
|
||||
if self._interrupt_requested:
|
||||
logger.info(f"[KFC] 动作筛选前被打断: {self.stream_id}")
|
||||
# 将当前处理的消息加入pending列表,下次一起处理
|
||||
if self._current_processing_message_id:
|
||||
self._pending_message_ids.add(self._current_processing_message_id)
|
||||
logger.info(f"[KFC] 消息 {self._current_processing_message_id} 加入pending列表")
|
||||
self._last_interrupt_time = time.time()
|
||||
self._current_processing_message_id = None
|
||||
return self._build_result(success=True, message="interrupted")
|
||||
|
||||
# V6: 使用ActionModifier筛选动作(复用AFC的三阶段筛选逻辑)
|
||||
# 阶段0: 聊天类型过滤(私聊/群聊)
|
||||
# 阶段2: 关联类型匹配(适配器能力检查)
|
||||
# 阶段3: 激活判定(go_activate + LLM判断)
|
||||
action_modifier = ActionModifier(
|
||||
action_manager=self.action_executor._action_manager,
|
||||
chat_id=self.stream_id,
|
||||
)
|
||||
await action_modifier.modify_actions(message_content=message_content)
|
||||
|
||||
# 获取筛选后的动作
|
||||
available_actions = self.action_executor._action_manager.get_using_actions()
|
||||
logger.info(
|
||||
f"[KFC] 动作筛选: {raw_action_count} -> {len(available_actions)} "
|
||||
f"(筛除 {raw_action_count - len(available_actions)} 个)"
|
||||
)
|
||||
|
||||
# 执行核心处理流程(传递筛选后的动作,V7: 传递所有未读消息)
|
||||
result = await self._handle_message(
|
||||
target_message,
|
||||
context,
|
||||
available_actions,
|
||||
all_unread_messages=unread_messages, # V7: 传递所有未读消息
|
||||
)
|
||||
|
||||
# 更新统计
|
||||
self.stats["messages_processed"] += 1
|
||||
|
||||
return result
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info(f"[KFC] 处理被取消: {self.stream_id}")
|
||||
self.stats["failed_responses"] += 1
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"[KFC] 处理出错: {e}\n{traceback.format_exc()}")
|
||||
self.stats["failed_responses"] += 1
|
||||
return self._build_result(
|
||||
success=False,
|
||||
message=str(e),
|
||||
error=True
|
||||
)
|
||||
finally:
|
||||
self._current_task = None
|
||||
|
||||
async def _handle_message(
|
||||
self,
|
||||
message: "DatabaseMessages",
|
||||
context: StreamContext,
|
||||
available_actions: dict | None = None,
|
||||
all_unread_messages: list | None = None, # V7: 所有未读消息(包含pending的)
|
||||
) -> dict:
|
||||
"""
|
||||
处理单条消息的核心逻辑
|
||||
|
||||
实现"体验 -> 决策 -> 行动"的交互模式
|
||||
V5超融合:集成S4U所有上下文模块
|
||||
V7升级:支持处理多条消息(打断机制合并pending消息)
|
||||
|
||||
Args:
|
||||
message: 要处理的主消息(最新的那条)
|
||||
context: 聊天上下文
|
||||
available_actions: 可用动作字典(V2新增)
|
||||
all_unread_messages: 所有未读消息列表(V7新增,包含pending消息)
|
||||
|
||||
Returns:
|
||||
处理结果字典
|
||||
"""
|
||||
# 1. 获取或创建会话
|
||||
user_id = str(message.user_info.user_id)
|
||||
session = await self.session_manager.get_session(user_id, self.stream_id)
|
||||
|
||||
# 2. 记录收到消息的事件
|
||||
await self._record_user_message(session, message)
|
||||
|
||||
# 3. 更新会话状态为RESPONDING
|
||||
old_status = session.status
|
||||
session.status = SessionStatus.RESPONDING
|
||||
|
||||
# 4. 如果之前在等待,结束等待状态
|
||||
if old_status == SessionStatus.WAITING:
|
||||
session.end_waiting()
|
||||
# V7: 用户回复了,重置连续追问计数
|
||||
session.consecutive_followup_count = 0
|
||||
logger.debug(f"[KFC] 收到消息,结束等待,重置追问计数: user={user_id}")
|
||||
|
||||
# 5. V5超融合:构建S4U上下文数据
|
||||
chat_stream = await self._get_chat_stream()
|
||||
context_data = {}
|
||||
|
||||
if chat_stream:
|
||||
try:
|
||||
context_builder = KFCContextBuilder(chat_stream)
|
||||
sender_name = message.user_info.user_nickname or user_id
|
||||
target_message = self._extract_message_content(message)
|
||||
|
||||
context_data = await context_builder.build_all_context(
|
||||
sender_name=sender_name,
|
||||
target_message=target_message,
|
||||
context=context,
|
||||
)
|
||||
logger.info(f"[KFC] 超融合上下文构建完成: {list(context_data.keys())}")
|
||||
except Exception as e:
|
||||
logger.warning(f"[KFC] 构建S4U上下文失败,使用基础模式: {e}")
|
||||
|
||||
# 6. 生成提示词(V3: 从共享数据源读取历史, V5: 传递S4U上下文, V7: 支持多条消息)
|
||||
system_prompt, user_prompt = self.prompt_generator.generate_responding_prompt(
|
||||
session=session,
|
||||
message_content=self._extract_message_content(message),
|
||||
sender_name=message.user_info.user_nickname or user_id,
|
||||
sender_id=user_id,
|
||||
message_time=message.time,
|
||||
available_actions=available_actions,
|
||||
context=context, # V3: 传递StreamContext以读取共享历史
|
||||
context_data=context_data, # V5: S4U上下文数据
|
||||
chat_stream=chat_stream, # V5: 聊天流用于场景判断
|
||||
all_unread_messages=all_unread_messages, # V7: 传递所有未读消息
|
||||
)
|
||||
|
||||
# 7. 调用LLM
|
||||
llm_response = await self._call_llm(system_prompt, user_prompt)
|
||||
self.stats["llm_calls"] += 1
|
||||
|
||||
# V7: LLM调用后检查是否被打断
|
||||
if self._interrupt_requested:
|
||||
logger.info(f"[KFC] LLM调用后被打断: {self.stream_id}")
|
||||
# 将当前处理的消息加入pending列表
|
||||
if self._current_processing_message_id:
|
||||
self._pending_message_ids.add(self._current_processing_message_id)
|
||||
logger.info(f"[KFC] 消息 {self._current_processing_message_id} 加入pending列表")
|
||||
self._last_interrupt_time = time.time()
|
||||
self._current_processing_message_id = None
|
||||
return self._build_result(success=True, message="interrupted_after_llm")
|
||||
|
||||
# 8. 解析响应
|
||||
parsed_response = self.action_executor.parse_llm_response(llm_response)
|
||||
|
||||
# 9. 执行动作
|
||||
execution_result = await self.action_executor.execute_actions(
|
||||
parsed_response,
|
||||
session,
|
||||
chat_stream
|
||||
)
|
||||
|
||||
# 10. 处理执行结果
|
||||
if execution_result["has_reply"]:
|
||||
# 如果发送了回复,检查是否需要进入等待状态
|
||||
max_wait = parsed_response.max_wait_seconds
|
||||
|
||||
if max_wait > 0:
|
||||
# 正常等待状态
|
||||
session.start_waiting(
|
||||
expected_reaction=parsed_response.expected_user_reaction,
|
||||
max_wait=max_wait
|
||||
)
|
||||
logger.debug(
|
||||
f"[KFC] 进入等待状态: user={user_id}, "
|
||||
f"max_wait={max_wait}s"
|
||||
)
|
||||
else:
|
||||
# max_wait=0 表示不等待(话题结束/用户说再见等)
|
||||
session.status = SessionStatus.IDLE
|
||||
session.end_waiting()
|
||||
logger.info(
|
||||
f"[KFC] 话题结束,不等待用户回复: user={user_id} "
|
||||
f"(max_wait_seconds=0)"
|
||||
)
|
||||
|
||||
session.total_interactions += 1
|
||||
self.stats["successful_responses"] += 1
|
||||
else:
|
||||
# 没有发送回复,返回空闲状态
|
||||
session.status = SessionStatus.IDLE
|
||||
logger.debug(f"[KFC] 无回复动作,返回空闲: user={user_id}")
|
||||
|
||||
# 11. 保存会话
|
||||
await self.session_manager.save_session(user_id)
|
||||
|
||||
# 12. V7: 标记当前消息为已读
|
||||
context.mark_message_as_read(str(message.message_id))
|
||||
|
||||
# 13. V7: 清除pending状态(所有消息都已成功处理)
|
||||
processed_count = len(self._pending_message_ids)
|
||||
if self._pending_message_ids:
|
||||
# 标记所有pending消息为已读
|
||||
for msg_id in self._pending_message_ids:
|
||||
context.mark_message_as_read(msg_id)
|
||||
logger.info(f"[KFC] 清除 {processed_count} 条pending消息: {self.stream_id}")
|
||||
self._pending_message_ids.clear()
|
||||
|
||||
# 清除当前处理的消息ID
|
||||
self._current_processing_message_id = None
|
||||
|
||||
return self._build_result(
|
||||
success=True,
|
||||
message="processed",
|
||||
has_reply=execution_result["has_reply"],
|
||||
thought=parsed_response.thought,
|
||||
pending_messages_processed=processed_count, # V7: 返回处理了多少条pending消息
|
||||
)
|
||||
|
||||
async def _record_user_message(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
message: "DatabaseMessages",
|
||||
) -> None:
|
||||
"""记录用户消息到会话历史"""
|
||||
content = self._extract_message_content(message)
|
||||
session.last_user_message = content
|
||||
|
||||
entry = MentalLogEntry(
|
||||
event_type=MentalLogEventType.USER_MESSAGE,
|
||||
timestamp=message.time or time.time(),
|
||||
thought="", # 用户消息不需要内心独白
|
||||
content=content,
|
||||
metadata={
|
||||
"message_id": str(message.message_id),
|
||||
"user_id": str(message.user_info.user_id),
|
||||
"user_name": message.user_info.user_nickname,
|
||||
},
|
||||
)
|
||||
session.add_mental_log_entry(entry)
|
||||
|
||||
def _extract_message_content(self, message: "DatabaseMessages") -> str:
|
||||
"""提取消息内容"""
|
||||
return (
|
||||
message.processed_plain_text
|
||||
or message.display_message
|
||||
or ""
|
||||
)
|
||||
|
||||
async def _call_llm(
|
||||
self,
|
||||
system_prompt: str,
|
||||
user_prompt: str,
|
||||
) -> str:
|
||||
"""
|
||||
调用LLM生成响应
|
||||
|
||||
Args:
|
||||
system_prompt: 系统提示词
|
||||
user_prompt: 用户提示词
|
||||
|
||||
Returns:
|
||||
LLM的响应文本
|
||||
"""
|
||||
try:
|
||||
# 获取模型配置
|
||||
# 使用 replyer 任务的模型配置(KFC 生成回复,必须使用回复专用模型)
|
||||
if model_config is None:
|
||||
raise RuntimeError("model_config 未初始化")
|
||||
task_config = model_config.model_task_config.replyer
|
||||
|
||||
llm_request = LLMRequest(
|
||||
model_set=task_config,
|
||||
request_type="kokoro_flow_chatter",
|
||||
)
|
||||
|
||||
# 构建完整的提示词(将系统提示词和用户提示词合并)
|
||||
full_prompt = f"{system_prompt}\n\n{user_prompt}"
|
||||
|
||||
# INFO日志:打印完整的KFC提示词(可观测性增强)
|
||||
logger.info(
|
||||
f"Final KFC prompt constructed for stream {self.stream_id}:\n"
|
||||
f"--- PROMPT START ---\n"
|
||||
f"[SYSTEM]\n{system_prompt}\n\n[USER]\n{user_prompt}\n"
|
||||
f"--- PROMPT END ---"
|
||||
)
|
||||
|
||||
# 生成响应
|
||||
response, _ = await llm_request.generate_response_async(
|
||||
prompt=full_prompt,
|
||||
)
|
||||
|
||||
# INFO日志:打印原始JSON响应(可观测性增强)
|
||||
logger.info(
|
||||
f"Raw JSON response from LLM for stream {self.stream_id}:\n"
|
||||
f"--- JSON START ---\n"
|
||||
f"{response}\n"
|
||||
f"--- JSON END ---"
|
||||
)
|
||||
|
||||
logger.info(f"[KFC] LLM响应长度: {len(response)}")
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[KFC] 调用LLM失败: {e}")
|
||||
# 返回一个默认的JSON响应
|
||||
return '{"thought": "出现了技术问题", "expected_user_reaction": "", "max_wait_seconds": 60, "actions": [{"type": "do_nothing"}]}'
|
||||
|
||||
async def _get_chat_stream(self, stream_id: Optional[str] = None):
|
||||
"""
|
||||
获取聊天流对象
|
||||
|
||||
Args:
|
||||
stream_id: 可选的stream_id,若不提供则使用self.stream_id
|
||||
在超时回调中应使用session.stream_id以避免发送到错误的用户
|
||||
"""
|
||||
target_stream_id = stream_id or self.stream_id
|
||||
try:
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
|
||||
chat_manager = get_chat_manager()
|
||||
if chat_manager:
|
||||
return await chat_manager.get_stream(target_stream_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"[KFC] 获取chat_stream失败 (stream_id={target_stream_id}): {e}")
|
||||
return None
|
||||
|
||||
async def _on_session_timeout(self, session: KokoroSession) -> None:
|
||||
"""
|
||||
会话超时回调(V7:增加连续追问限制)
|
||||
|
||||
当等待超时时,触发后续决策流程
|
||||
|
||||
注意:此回调由全局调度器触发,可能会在任意Chatter实例上执行。
|
||||
因此必须使用session.stream_id而非self.stream_id来确保消息发送给正确的用户。
|
||||
|
||||
Args:
|
||||
session: 超时的会话
|
||||
"""
|
||||
logger.info(f"[KFC] 处理超时决策: user={session.user_id}, stream_id={session.stream_id}, followup_count={session.consecutive_followup_count}")
|
||||
self.stats["timeout_decisions"] += 1
|
||||
|
||||
try:
|
||||
# V7: 检查是否超过最大连续追问次数
|
||||
if session.consecutive_followup_count >= session.max_consecutive_followups:
|
||||
logger.info(
|
||||
f"[KFC] 已达到最大连续追问次数 ({session.max_consecutive_followups}),"
|
||||
f"自动返回IDLE状态: user={session.user_id}"
|
||||
)
|
||||
session.status = SessionStatus.IDLE
|
||||
session.end_waiting()
|
||||
# 重置连续追问计数(下次用户回复后会重新开始)
|
||||
session.consecutive_followup_count = 0
|
||||
await self.session_manager.save_session(session.user_id)
|
||||
return
|
||||
|
||||
# 关键修复:使用 session 的 stream_id 创建正确的 ActionExecutor
|
||||
# 因为全局调度器的回调可能在任意 Chatter 实例上执行
|
||||
from .action_executor import ActionExecutor
|
||||
timeout_action_executor = ActionExecutor(session.stream_id)
|
||||
|
||||
# V2: 加载可用动作
|
||||
available_actions = await timeout_action_executor.load_actions()
|
||||
|
||||
# 生成超时决策提示词(V2: 传递可用动作,V7: 传递连续追问信息)
|
||||
system_prompt, user_prompt = self.prompt_generator.generate_timeout_decision_prompt(
|
||||
session,
|
||||
available_actions=available_actions,
|
||||
)
|
||||
|
||||
# 调用LLM
|
||||
llm_response = await self._call_llm(system_prompt, user_prompt)
|
||||
self.stats["llm_calls"] += 1
|
||||
|
||||
# 解析响应
|
||||
parsed_response = timeout_action_executor.parse_llm_response(llm_response)
|
||||
|
||||
# 关键修复:使用 session.stream_id 获取正确的 chat_stream
|
||||
chat_stream = await self._get_chat_stream(session.stream_id)
|
||||
execution_result = await timeout_action_executor.execute_actions(
|
||||
parsed_response,
|
||||
session,
|
||||
chat_stream
|
||||
)
|
||||
|
||||
# 更新会话状态
|
||||
if execution_result["has_reply"]:
|
||||
# V7: 发送了后续消息,增加连续追问计数
|
||||
session.consecutive_followup_count += 1
|
||||
logger.info(f"[KFC] 发送追问消息,当前连续追问次数: {session.consecutive_followup_count}")
|
||||
|
||||
# 如果发送了后续消息,重新进入等待
|
||||
session.start_waiting(
|
||||
expected_reaction=parsed_response.expected_user_reaction,
|
||||
max_wait=parsed_response.max_wait_seconds
|
||||
)
|
||||
else:
|
||||
# V7重构:do_nothing 的两种情况
|
||||
# 1. max_wait_seconds > 0: "看了一眼手机,决定再等等" → 继续等待,不算追问
|
||||
# 2. max_wait_seconds = 0: "算了,不等了" → 进入 IDLE
|
||||
if parsed_response.max_wait_seconds > 0:
|
||||
# 继续等待,不增加追问计数
|
||||
logger.info(
|
||||
f"[KFC] 决定继续等待 {parsed_response.max_wait_seconds}s,"
|
||||
f"不算追问: user={session.user_id}"
|
||||
)
|
||||
session.start_waiting(
|
||||
expected_reaction=parsed_response.expected_user_reaction or session.expected_user_reaction,
|
||||
max_wait=parsed_response.max_wait_seconds
|
||||
)
|
||||
else:
|
||||
# 不再等待,进入 IDLE
|
||||
logger.info(f"[KFC] 决定不再等待,返回IDLE: user={session.user_id}")
|
||||
session.status = SessionStatus.IDLE
|
||||
session.end_waiting()
|
||||
|
||||
# 保存会话
|
||||
await self.session_manager.save_session(session.user_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[KFC] 超时决策处理失败: {e}")
|
||||
# 发生错误时返回空闲状态
|
||||
session.status = SessionStatus.IDLE
|
||||
session.end_waiting()
|
||||
await self.session_manager.save_session(session.user_id)
|
||||
|
||||
async def _on_continuous_thinking(self, session: KokoroSession) -> None:
|
||||
"""
|
||||
连续思考回调(V2升级版)
|
||||
|
||||
在等待期间更新心理状态,可选择调用LLM生成更自然的想法
|
||||
V2: 支持通过配置启用LLM驱动的连续思考
|
||||
|
||||
Args:
|
||||
session: 会话
|
||||
"""
|
||||
logger.debug(f"[KFC] 连续思考触发: user={session.user_id}")
|
||||
|
||||
# 检查是否启用LLM驱动的连续思考
|
||||
use_llm_thinking = self.get_config(
|
||||
"behavior.use_llm_continuous_thinking",
|
||||
default=False
|
||||
)
|
||||
|
||||
if use_llm_thinking and isinstance(use_llm_thinking, bool) and use_llm_thinking:
|
||||
try:
|
||||
# V2: 加载可用动作
|
||||
available_actions = await self.action_executor.load_actions()
|
||||
|
||||
# 生成连续思考提示词
|
||||
system_prompt, user_prompt = self.prompt_generator.generate_continuous_thinking_prompt(
|
||||
session,
|
||||
available_actions=available_actions,
|
||||
)
|
||||
|
||||
# 调用LLM
|
||||
llm_response = await self._call_llm(system_prompt, user_prompt)
|
||||
self.stats["llm_calls"] += 1
|
||||
|
||||
# 解析并执行(可能会更新内部状态)
|
||||
parsed_response = self.action_executor.parse_llm_response(llm_response)
|
||||
|
||||
# 只执行内部动作,不执行外部动作
|
||||
for action in parsed_response.actions:
|
||||
if action.type == "update_internal_state":
|
||||
await self.action_executor._execute_internal_action(action, session)
|
||||
|
||||
# 记录思考内容
|
||||
entry = MentalLogEntry(
|
||||
event_type=MentalLogEventType.CONTINUOUS_THINKING,
|
||||
timestamp=time.time(),
|
||||
thought=parsed_response.thought,
|
||||
content="",
|
||||
emotional_snapshot=session.emotional_state.to_dict(),
|
||||
)
|
||||
session.add_mental_log_entry(entry)
|
||||
|
||||
# 保存会话
|
||||
await self.session_manager.save_session(session.user_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"[KFC] LLM连续思考失败: {e}")
|
||||
|
||||
# 简单模式:更新焦虑程度(已在scheduler中处理)
|
||||
# 这里可以添加额外的逻辑
|
||||
|
||||
async def _on_proactive_thinking(self, session: KokoroSession, trigger_reason: str) -> None:
|
||||
"""
|
||||
主动思考回调
|
||||
|
||||
当长时间沉默后触发,让 LLM 决定是否主动联系用户。
|
||||
这不是"必须发消息",而是"想一想要不要联系对方"。
|
||||
|
||||
Args:
|
||||
session: 会话
|
||||
trigger_reason: 触发原因描述
|
||||
"""
|
||||
logger.info(f"[KFC] 处理主动思考: user={session.user_id}, reason={trigger_reason}")
|
||||
|
||||
try:
|
||||
# 创建正确的 ActionExecutor(使用 session 的 stream_id)
|
||||
from .action_executor import ActionExecutor
|
||||
proactive_action_executor = ActionExecutor(session.stream_id)
|
||||
|
||||
# 加载可用动作
|
||||
available_actions = await proactive_action_executor.load_actions()
|
||||
|
||||
# 获取 chat_stream 用于构建上下文
|
||||
chat_stream = await self._get_chat_stream(session.stream_id)
|
||||
|
||||
# 构建 S4U 上下文数据(包含全局关系信息)
|
||||
context_data: dict[str, str] = {}
|
||||
if chat_stream:
|
||||
try:
|
||||
from .context_builder import KFCContextBuilder
|
||||
context_builder = KFCContextBuilder(chat_stream)
|
||||
context_data = await context_builder.build_all_context(
|
||||
sender_name=session.user_id, # 主动思考时用 user_id
|
||||
target_message="", # 没有目标消息
|
||||
context=None,
|
||||
)
|
||||
logger.debug(f"[KFC] 主动思考上下文构建完成: {list(context_data.keys())}")
|
||||
except Exception as e:
|
||||
logger.warning(f"[KFC] 主动思考构建S4U上下文失败: {e}")
|
||||
|
||||
# 生成主动思考提示词(传入 context_data 以获取全局关系信息)
|
||||
system_prompt, user_prompt = self.prompt_generator.generate_proactive_thinking_prompt(
|
||||
session,
|
||||
trigger_context=trigger_reason,
|
||||
available_actions=available_actions,
|
||||
context_data=context_data,
|
||||
chat_stream=chat_stream,
|
||||
)
|
||||
|
||||
# 调用 LLM
|
||||
llm_response = await self._call_llm(system_prompt, user_prompt)
|
||||
self.stats["llm_calls"] += 1
|
||||
|
||||
# 解析响应
|
||||
parsed_response = proactive_action_executor.parse_llm_response(llm_response)
|
||||
|
||||
# 检查是否决定不打扰(do_nothing)
|
||||
is_do_nothing = (
|
||||
len(parsed_response.actions) == 0 or
|
||||
(len(parsed_response.actions) == 1 and parsed_response.actions[0].type == "do_nothing")
|
||||
)
|
||||
|
||||
if is_do_nothing:
|
||||
logger.info(f"[KFC] 主动思考决定不打扰: user={session.user_id}, thought={parsed_response.thought[:50]}...")
|
||||
# 记录这次"决定不打扰"的思考
|
||||
entry = MentalLogEntry(
|
||||
event_type=MentalLogEventType.PROACTIVE_THINKING,
|
||||
timestamp=time.time(),
|
||||
thought=parsed_response.thought,
|
||||
content="决定不打扰",
|
||||
emotional_snapshot=session.emotional_state.to_dict(),
|
||||
metadata={"trigger_reason": trigger_reason, "action": "do_nothing"},
|
||||
)
|
||||
session.add_mental_log_entry(entry)
|
||||
await self.session_manager.save_session(session.user_id)
|
||||
return
|
||||
|
||||
# 执行决定的动作
|
||||
execution_result = await proactive_action_executor.execute_actions(
|
||||
parsed_response,
|
||||
session,
|
||||
chat_stream
|
||||
)
|
||||
|
||||
logger.info(f"[KFC] 主动思考执行完成: user={session.user_id}, has_reply={execution_result.get('has_reply')}")
|
||||
|
||||
# 如果发送了消息,进入等待状态
|
||||
if execution_result.get("has_reply"):
|
||||
session.start_waiting(
|
||||
expected_reaction=parsed_response.expected_user_reaction,
|
||||
max_wait=parsed_response.max_wait_seconds
|
||||
)
|
||||
|
||||
# 保存会话
|
||||
await self.session_manager.save_session(session.user_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[KFC] 主动思考处理失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _build_result(
|
||||
self,
|
||||
success: bool,
|
||||
message: str = "",
|
||||
error: bool = False,
|
||||
**kwargs,
|
||||
) -> dict:
|
||||
"""构建返回结果"""
|
||||
result = {
|
||||
"success": success,
|
||||
"stream_id": self.stream_id,
|
||||
"message": message,
|
||||
"error": error,
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
result.update(kwargs)
|
||||
return result
|
||||
|
||||
def get_stats(self) -> dict[str, Any]:
|
||||
"""获取统计信息"""
|
||||
return {
|
||||
**self.stats,
|
||||
"last_activity_time": self.last_activity_time,
|
||||
"action_executor_stats": self.action_executor.get_execution_stats(),
|
||||
}
|
||||
|
||||
def reset_stats(self) -> None:
|
||||
"""重置统计信息"""
|
||||
self.stats = {
|
||||
"messages_processed": 0,
|
||||
"llm_calls": 0,
|
||||
"successful_responses": 0,
|
||||
"failed_responses": 0,
|
||||
"timeout_decisions": 0,
|
||||
"interrupts": 0, # V7: 打断次数统计
|
||||
}
|
||||
self.action_executor.reset_stats()
|
||||
|
||||
async def get_session_info(self) -> Optional[dict]:
|
||||
"""获取当前会话信息(用于调试)"""
|
||||
try:
|
||||
# 尝试获取当前用户的会话
|
||||
sessions = await self.session_manager.get_all_waiting_sessions()
|
||||
for session in sessions:
|
||||
if session.stream_id == self.stream_id:
|
||||
return session.to_dict()
|
||||
except Exception as e:
|
||||
logger.error(f"获取会话信息失败: {e}")
|
||||
return None
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""字符串表示"""
|
||||
return f"KokoroFlowChatter(stream_id={self.stream_id})"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""详细字符串表示"""
|
||||
return (
|
||||
f"KokoroFlowChatter(stream_id={self.stream_id}, "
|
||||
f"messages_processed={self.stats['messages_processed']})"
|
||||
)
|
||||
@@ -1,251 +0,0 @@
|
||||
"""
|
||||
Kokoro Flow Chatter 插件配置
|
||||
|
||||
定义插件的配置项和默认值
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class PersonaConfig:
|
||||
"""人设配置"""
|
||||
description: str = """你是一个温暖、真诚的对话伙伴。
|
||||
你有自己的情感和想法,会真实地表达内心的感受。
|
||||
你关心与用户的关系,但也保持健康的边界。
|
||||
你可以感到开心、好奇、担心、或者稍微沮丧,这些都是正常的。"""
|
||||
|
||||
name: str = "心流助手"
|
||||
"""AI的名字"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class BehaviorConfig:
|
||||
"""行为配置"""
|
||||
max_wait_seconds_default: int = 300
|
||||
"""默认最大等待秒数"""
|
||||
|
||||
min_wait_seconds: int = 60
|
||||
"""最小等待秒数"""
|
||||
|
||||
max_wait_seconds_limit: int = 600
|
||||
"""最大等待秒数上限"""
|
||||
|
||||
enable_continuous_thinking: bool = True
|
||||
"""是否启用连续思考"""
|
||||
|
||||
continuous_thinking_triggers: list[float] = field(
|
||||
default_factory=lambda: [0.3, 0.6, 0.85]
|
||||
)
|
||||
"""连续思考触发点(等待进度百分比)"""
|
||||
|
||||
scheduler_check_interval: float = 10.0
|
||||
"""调度器检查间隔(秒)"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class SessionConfig:
|
||||
"""会话配置"""
|
||||
data_dir: str = "data/kokoro_flow_chatter/sessions"
|
||||
"""会话数据存储目录"""
|
||||
|
||||
max_session_age_days: int = 30
|
||||
"""会话最大保留天数"""
|
||||
|
||||
auto_save_interval: int = 300
|
||||
"""自动保存间隔(秒)"""
|
||||
|
||||
max_mental_log_size: int = 100
|
||||
"""心理日志最大条目数"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMConfig:
|
||||
"""LLM配置"""
|
||||
model_name: str = ""
|
||||
"""使用的模型名称,留空则使用默认主模型"""
|
||||
|
||||
max_tokens: int = 2048
|
||||
"""最大生成token数"""
|
||||
|
||||
temperature: float = 0.8
|
||||
"""生成温度"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class EmotionalConfig:
|
||||
"""情感系统配置"""
|
||||
initial_mood: str = "neutral"
|
||||
"""初始心情"""
|
||||
|
||||
initial_mood_intensity: float = 0.5
|
||||
"""初始心情强度"""
|
||||
|
||||
initial_relationship_warmth: float = 0.5
|
||||
"""初始关系热度"""
|
||||
|
||||
anxiety_increase_rate: float = 0.5
|
||||
"""焦虑增长率(平方根系数)"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class KokoroFlowChatterConfig:
|
||||
"""心流聊天器完整配置"""
|
||||
enabled: bool = True
|
||||
"""是否启用插件"""
|
||||
|
||||
persona: PersonaConfig = field(default_factory=PersonaConfig)
|
||||
"""人设配置"""
|
||||
|
||||
behavior: BehaviorConfig = field(default_factory=BehaviorConfig)
|
||||
"""行为配置"""
|
||||
|
||||
session: SessionConfig = field(default_factory=SessionConfig)
|
||||
"""会话配置"""
|
||||
|
||||
llm: LLMConfig = field(default_factory=LLMConfig)
|
||||
"""LLM配置"""
|
||||
|
||||
emotional: EmotionalConfig = field(default_factory=EmotionalConfig)
|
||||
"""情感系统配置"""
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"enabled": self.enabled,
|
||||
"persona": {
|
||||
"description": self.persona.description,
|
||||
"name": self.persona.name,
|
||||
},
|
||||
"behavior": {
|
||||
"max_wait_seconds_default": self.behavior.max_wait_seconds_default,
|
||||
"min_wait_seconds": self.behavior.min_wait_seconds,
|
||||
"max_wait_seconds_limit": self.behavior.max_wait_seconds_limit,
|
||||
"enable_continuous_thinking": self.behavior.enable_continuous_thinking,
|
||||
"continuous_thinking_triggers": self.behavior.continuous_thinking_triggers,
|
||||
"scheduler_check_interval": self.behavior.scheduler_check_interval,
|
||||
},
|
||||
"session": {
|
||||
"data_dir": self.session.data_dir,
|
||||
"max_session_age_days": self.session.max_session_age_days,
|
||||
"auto_save_interval": self.session.auto_save_interval,
|
||||
"max_mental_log_size": self.session.max_mental_log_size,
|
||||
},
|
||||
"llm": {
|
||||
"model_name": self.llm.model_name,
|
||||
"max_tokens": self.llm.max_tokens,
|
||||
"temperature": self.llm.temperature,
|
||||
},
|
||||
"emotional": {
|
||||
"initial_mood": self.emotional.initial_mood,
|
||||
"initial_mood_intensity": self.emotional.initial_mood_intensity,
|
||||
"initial_relationship_warmth": self.emotional.initial_relationship_warmth,
|
||||
"anxiety_increase_rate": self.emotional.anxiety_increase_rate,
|
||||
},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "KokoroFlowChatterConfig":
|
||||
"""从字典创建配置"""
|
||||
config = cls()
|
||||
|
||||
if "enabled" in data:
|
||||
config.enabled = data["enabled"]
|
||||
|
||||
if "persona" in data:
|
||||
persona_data = data["persona"]
|
||||
config.persona.description = persona_data.get(
|
||||
"description",
|
||||
config.persona.description
|
||||
)
|
||||
config.persona.name = persona_data.get(
|
||||
"name",
|
||||
config.persona.name
|
||||
)
|
||||
|
||||
if "behavior" in data:
|
||||
behavior_data = data["behavior"]
|
||||
config.behavior.max_wait_seconds_default = behavior_data.get(
|
||||
"max_wait_seconds_default",
|
||||
config.behavior.max_wait_seconds_default
|
||||
)
|
||||
config.behavior.min_wait_seconds = behavior_data.get(
|
||||
"min_wait_seconds",
|
||||
config.behavior.min_wait_seconds
|
||||
)
|
||||
config.behavior.max_wait_seconds_limit = behavior_data.get(
|
||||
"max_wait_seconds_limit",
|
||||
config.behavior.max_wait_seconds_limit
|
||||
)
|
||||
config.behavior.enable_continuous_thinking = behavior_data.get(
|
||||
"enable_continuous_thinking",
|
||||
config.behavior.enable_continuous_thinking
|
||||
)
|
||||
config.behavior.continuous_thinking_triggers = behavior_data.get(
|
||||
"continuous_thinking_triggers",
|
||||
config.behavior.continuous_thinking_triggers
|
||||
)
|
||||
config.behavior.scheduler_check_interval = behavior_data.get(
|
||||
"scheduler_check_interval",
|
||||
config.behavior.scheduler_check_interval
|
||||
)
|
||||
|
||||
if "session" in data:
|
||||
session_data = data["session"]
|
||||
config.session.data_dir = session_data.get(
|
||||
"data_dir",
|
||||
config.session.data_dir
|
||||
)
|
||||
config.session.max_session_age_days = session_data.get(
|
||||
"max_session_age_days",
|
||||
config.session.max_session_age_days
|
||||
)
|
||||
config.session.auto_save_interval = session_data.get(
|
||||
"auto_save_interval",
|
||||
config.session.auto_save_interval
|
||||
)
|
||||
config.session.max_mental_log_size = session_data.get(
|
||||
"max_mental_log_size",
|
||||
config.session.max_mental_log_size
|
||||
)
|
||||
|
||||
if "llm" in data:
|
||||
llm_data = data["llm"]
|
||||
config.llm.model_name = llm_data.get(
|
||||
"model_name",
|
||||
config.llm.model_name
|
||||
)
|
||||
config.llm.max_tokens = llm_data.get(
|
||||
"max_tokens",
|
||||
config.llm.max_tokens
|
||||
)
|
||||
config.llm.temperature = llm_data.get(
|
||||
"temperature",
|
||||
config.llm.temperature
|
||||
)
|
||||
|
||||
if "emotional" in data:
|
||||
emotional_data = data["emotional"]
|
||||
config.emotional.initial_mood = emotional_data.get(
|
||||
"initial_mood",
|
||||
config.emotional.initial_mood
|
||||
)
|
||||
config.emotional.initial_mood_intensity = emotional_data.get(
|
||||
"initial_mood_intensity",
|
||||
config.emotional.initial_mood_intensity
|
||||
)
|
||||
config.emotional.initial_relationship_warmth = emotional_data.get(
|
||||
"initial_relationship_warmth",
|
||||
config.emotional.initial_relationship_warmth
|
||||
)
|
||||
config.emotional.anxiety_increase_rate = emotional_data.get(
|
||||
"anxiety_increase_rate",
|
||||
config.emotional.anxiety_increase_rate
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
# 默认配置实例
|
||||
default_config = KokoroFlowChatterConfig()
|
||||
@@ -1,528 +0,0 @@
|
||||
"""
|
||||
Kokoro Flow Chatter 上下文构建器
|
||||
|
||||
该模块负责从 S4U 移植的所有上下文模块,为 KFC 提供"全知"Prompt所需的完整情境感知能力。
|
||||
包含:
|
||||
- 关系信息 (relation_info)
|
||||
- 记忆块 (memory_block)
|
||||
- 表达习惯 (expression_habits)
|
||||
- 知识库 (knowledge)
|
||||
- 跨上下文 (cross_context)
|
||||
- 日程信息 (schedule)
|
||||
- 通知块 (notice)
|
||||
- 历史消息构建 (history)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import global_config
|
||||
from src.person_info.person_info import get_person_info_manager, PersonInfoManager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.chat.message_receive.chat_stream import ChatStream
|
||||
from src.common.data_models.message_manager_data_model import StreamContext
|
||||
from src.config.config import BotConfig # 用于类型提示
|
||||
|
||||
logger = get_logger("kfc_context_builder")
|
||||
|
||||
|
||||
# 类型断言辅助函数
|
||||
def _get_config():
|
||||
"""获取全局配置(带类型断言)"""
|
||||
assert global_config is not None, "global_config 未初始化"
|
||||
return global_config
|
||||
|
||||
|
||||
class KFCContextBuilder:
|
||||
"""
|
||||
KFC 上下文构建器
|
||||
|
||||
从 S4U 的 DefaultReplyer 移植所有上下文构建能力,
|
||||
为 KFC 的"超融合"Prompt 提供完整的情境感知数据。
|
||||
"""
|
||||
|
||||
def __init__(self, chat_stream: "ChatStream"):
|
||||
"""
|
||||
初始化上下文构建器
|
||||
|
||||
Args:
|
||||
chat_stream: 当前聊天流
|
||||
"""
|
||||
self.chat_stream = chat_stream
|
||||
self.chat_id = chat_stream.stream_id
|
||||
self.platform = chat_stream.platform
|
||||
self.is_group_chat = bool(chat_stream.group_info)
|
||||
|
||||
# 延迟初始化的组件
|
||||
self._tool_executor: Any = None
|
||||
self._expression_selector: Any = None
|
||||
|
||||
@property
|
||||
def tool_executor(self) -> Any:
|
||||
"""延迟初始化工具执行器"""
|
||||
if self._tool_executor is None:
|
||||
from src.plugin_system.core.tool_use import ToolExecutor
|
||||
self._tool_executor = ToolExecutor(chat_id=self.chat_id)
|
||||
return self._tool_executor
|
||||
|
||||
async def build_all_context(
|
||||
self,
|
||||
sender_name: str,
|
||||
target_message: str,
|
||||
context: Optional["StreamContext"] = None,
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
并行构建所有上下文模块
|
||||
|
||||
Args:
|
||||
sender_name: 发送者名称
|
||||
target_message: 目标消息内容
|
||||
context: 聊天流上下文(可选)
|
||||
|
||||
Returns:
|
||||
dict: 包含所有上下文块的字典
|
||||
"""
|
||||
# 获取历史消息用于构建各种上下文
|
||||
chat_history = await self._get_chat_history_text(context)
|
||||
|
||||
# 并行执行所有上下文构建任务
|
||||
tasks = {
|
||||
"relation_info": self._build_relation_info(sender_name, target_message),
|
||||
"memory_block": self._build_memory_block(chat_history, target_message),
|
||||
"expression_habits": self._build_expression_habits(chat_history, target_message),
|
||||
"schedule": self._build_schedule_block(),
|
||||
"time": self._build_time_block(),
|
||||
}
|
||||
|
||||
results = {}
|
||||
try:
|
||||
task_results = await asyncio.gather(
|
||||
*[self._wrap_task(name, coro) for name, coro in tasks.items()],
|
||||
return_exceptions=True
|
||||
)
|
||||
|
||||
for result in task_results:
|
||||
if isinstance(result, tuple):
|
||||
name, value = result
|
||||
results[name] = value
|
||||
else:
|
||||
logger.warning(f"上下文构建任务异常: {result}")
|
||||
except Exception as e:
|
||||
logger.error(f"并行构建上下文失败: {e}")
|
||||
|
||||
return results
|
||||
|
||||
async def _wrap_task(self, name: str, coro) -> tuple[str, str]:
|
||||
"""包装任务以返回名称和结果"""
|
||||
try:
|
||||
result = await coro
|
||||
return (name, result or "")
|
||||
except Exception as e:
|
||||
logger.error(f"构建 {name} 失败: {e}")
|
||||
return (name, "")
|
||||
|
||||
async def _get_chat_history_text(
|
||||
self,
|
||||
context: Optional["StreamContext"] = None,
|
||||
limit: int = 20,
|
||||
) -> str:
|
||||
"""
|
||||
获取聊天历史文本
|
||||
|
||||
Args:
|
||||
context: 聊天流上下文
|
||||
limit: 最大消息数量
|
||||
|
||||
Returns:
|
||||
str: 格式化的聊天历史
|
||||
"""
|
||||
if context is None:
|
||||
return ""
|
||||
|
||||
try:
|
||||
from src.chat.utils.chat_message_builder import build_readable_messages
|
||||
|
||||
messages = context.get_messages(limit=limit, include_unread=True)
|
||||
if not messages:
|
||||
return ""
|
||||
|
||||
# 转换为字典格式
|
||||
msg_dicts = [msg.flatten() for msg in messages]
|
||||
|
||||
return await build_readable_messages(
|
||||
msg_dicts,
|
||||
replace_bot_name=True,
|
||||
timestamp_mode="relative",
|
||||
truncate=True,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"获取聊天历史失败: {e}")
|
||||
return ""
|
||||
|
||||
async def _build_relation_info(self, sender_name: str, target_message: str) -> str:
|
||||
"""
|
||||
构建关系信息块
|
||||
|
||||
从 S4U 的 build_relation_info 移植
|
||||
|
||||
Args:
|
||||
sender_name: 发送者名称
|
||||
target_message: 目标消息
|
||||
|
||||
Returns:
|
||||
str: 格式化的关系信息
|
||||
"""
|
||||
config = _get_config()
|
||||
|
||||
# 检查是否是Bot自己的消息
|
||||
if sender_name == f"{config.bot.nickname}(你)":
|
||||
return "你将要回复的是你自己发送的消息。"
|
||||
|
||||
person_info_manager = get_person_info_manager()
|
||||
person_id = await person_info_manager.get_person_id_by_person_name(sender_name)
|
||||
|
||||
if not person_id:
|
||||
logger.debug(f"未找到用户 {sender_name} 的ID")
|
||||
return f"你完全不认识{sender_name},这是你们的第一次互动。"
|
||||
|
||||
try:
|
||||
from src.person_info.relationship_fetcher import relationship_fetcher_manager
|
||||
|
||||
relationship_fetcher = relationship_fetcher_manager.get_fetcher(self.chat_id)
|
||||
|
||||
# 构建用户关系信息(包含别名、偏好关键词等字段)
|
||||
user_relation_info = await relationship_fetcher.build_relation_info(person_id, points_num=5)
|
||||
|
||||
# 构建聊天流印象信息(群聊/私聊的整体印象)
|
||||
stream_impression = await relationship_fetcher.build_chat_stream_impression(self.chat_id)
|
||||
|
||||
# 组合信息
|
||||
parts = []
|
||||
if user_relation_info:
|
||||
parts.append(f"### 你与 {sender_name} 的关系\n{user_relation_info}")
|
||||
if stream_impression:
|
||||
scene_type = "这个群" if self.is_group_chat else "你们的私聊"
|
||||
parts.append(f"### 你对{scene_type}的印象\n{stream_impression}")
|
||||
|
||||
if parts:
|
||||
return "\n\n".join(parts)
|
||||
else:
|
||||
return f"你与{sender_name}还没有建立深厚的关系,这是早期的互动阶段。"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取关系信息失败: {e}")
|
||||
return self._build_fallback_relation_info(sender_name, person_id)
|
||||
|
||||
def _build_fallback_relation_info(self, sender_name: str, person_id: str) -> str:
|
||||
"""降级的关系信息构建"""
|
||||
return f"你与{sender_name}是普通朋友关系。"
|
||||
|
||||
async def _build_memory_block(self, chat_history: str, target_message: str) -> str:
|
||||
"""
|
||||
构建记忆块
|
||||
|
||||
从 S4U 的 build_memory_block 移植,使用三层记忆系统
|
||||
|
||||
Args:
|
||||
chat_history: 聊天历史
|
||||
target_message: 目标消息
|
||||
|
||||
Returns:
|
||||
str: 格式化的记忆信息
|
||||
"""
|
||||
config = _get_config()
|
||||
|
||||
if not (config.memory and config.memory.enable):
|
||||
return ""
|
||||
|
||||
try:
|
||||
from src.memory_graph.manager_singleton import get_unified_memory_manager
|
||||
from src.memory_graph.utils.three_tier_formatter import memory_formatter
|
||||
|
||||
unified_manager = get_unified_memory_manager()
|
||||
if not unified_manager:
|
||||
logger.debug("[三层记忆] 管理器未初始化")
|
||||
return ""
|
||||
|
||||
# 使用统一管理器的智能检索
|
||||
search_result = await unified_manager.search_memories(
|
||||
query_text=target_message,
|
||||
use_judge=True,
|
||||
recent_chat_history=chat_history,
|
||||
)
|
||||
|
||||
if not search_result:
|
||||
return ""
|
||||
|
||||
# 分类记忆块
|
||||
perceptual_blocks = search_result.get("perceptual_blocks", [])
|
||||
short_term_memories = search_result.get("short_term_memories", [])
|
||||
long_term_memories = search_result.get("long_term_memories", [])
|
||||
|
||||
# 使用三级记忆格式化器
|
||||
formatted_memories = await memory_formatter.format_all_tiers(
|
||||
perceptual_blocks=perceptual_blocks,
|
||||
short_term_memories=short_term_memories,
|
||||
long_term_memories=long_term_memories
|
||||
)
|
||||
|
||||
total_count = len(perceptual_blocks) + len(short_term_memories) + len(long_term_memories)
|
||||
if total_count > 0 and formatted_memories.strip():
|
||||
logger.info(
|
||||
f"[三层记忆] 检索到 {total_count} 条记忆 "
|
||||
f"(感知:{len(perceptual_blocks)}, 短期:{len(short_term_memories)}, 长期:{len(long_term_memories)})"
|
||||
)
|
||||
return f"### 🧠 相关记忆\n\n{formatted_memories}"
|
||||
|
||||
return ""
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[三层记忆] 检索失败: {e}")
|
||||
return ""
|
||||
|
||||
async def _build_expression_habits(self, chat_history: str, target_message: str) -> str:
|
||||
"""
|
||||
构建表达习惯块
|
||||
|
||||
从 S4U 的 build_expression_habits 移植
|
||||
|
||||
Args:
|
||||
chat_history: 聊天历史
|
||||
target_message: 目标消息
|
||||
|
||||
Returns:
|
||||
str: 格式化的表达习惯
|
||||
"""
|
||||
config = _get_config()
|
||||
|
||||
# 检查是否允许使用表达
|
||||
use_expression, _, _ = config.expression.get_expression_config_for_chat(self.chat_id)
|
||||
if not use_expression:
|
||||
return ""
|
||||
|
||||
try:
|
||||
from src.chat.express.expression_selector import expression_selector
|
||||
|
||||
style_habits = []
|
||||
grammar_habits = []
|
||||
|
||||
# 使用统一的表达方式选择
|
||||
selected_expressions = await expression_selector.select_suitable_expressions(
|
||||
chat_id=self.chat_id,
|
||||
chat_history=chat_history,
|
||||
target_message=target_message,
|
||||
max_num=8,
|
||||
min_num=2
|
||||
)
|
||||
|
||||
if selected_expressions:
|
||||
for expr in selected_expressions:
|
||||
if isinstance(expr, dict) and "situation" in expr and "style" in expr:
|
||||
expr_type = expr.get("type", "style")
|
||||
habit_str = f"当{expr['situation']}时,使用 {expr['style']}"
|
||||
if expr_type == "grammar":
|
||||
grammar_habits.append(habit_str)
|
||||
else:
|
||||
style_habits.append(habit_str)
|
||||
|
||||
# 构建表达习惯块
|
||||
parts = []
|
||||
if style_habits:
|
||||
parts.append("**语言风格习惯**:\n" + "\n".join(f"- {h}" for h in style_habits))
|
||||
if grammar_habits:
|
||||
parts.append("**句法习惯**:\n" + "\n".join(f"- {h}" for h in grammar_habits))
|
||||
|
||||
if parts:
|
||||
return "### 💬 你的表达习惯\n\n" + "\n\n".join(parts)
|
||||
|
||||
return ""
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"构建表达习惯失败: {e}")
|
||||
return ""
|
||||
|
||||
async def _build_schedule_block(self) -> str:
|
||||
"""
|
||||
构建日程信息块
|
||||
|
||||
从 S4U 移植
|
||||
|
||||
Returns:
|
||||
str: 格式化的日程信息
|
||||
"""
|
||||
config = _get_config()
|
||||
|
||||
if not config.planning_system.schedule_enable:
|
||||
return ""
|
||||
|
||||
try:
|
||||
from src.schedule.schedule_manager import schedule_manager
|
||||
|
||||
activity_info = schedule_manager.get_current_activity()
|
||||
if not activity_info:
|
||||
return ""
|
||||
|
||||
activity = activity_info.get("activity")
|
||||
time_range = activity_info.get("time_range")
|
||||
now = datetime.now()
|
||||
|
||||
if time_range:
|
||||
try:
|
||||
start_str, end_str = time_range.split("-")
|
||||
start_time = datetime.strptime(start_str.strip(), "%H:%M").replace(
|
||||
year=now.year, month=now.month, day=now.day
|
||||
)
|
||||
end_time = datetime.strptime(end_str.strip(), "%H:%M").replace(
|
||||
year=now.year, month=now.month, day=now.day
|
||||
)
|
||||
|
||||
if end_time < start_time:
|
||||
end_time += timedelta(days=1)
|
||||
if now < start_time:
|
||||
now += timedelta(days=1)
|
||||
|
||||
duration_minutes = (now - start_time).total_seconds() / 60
|
||||
remaining_minutes = (end_time - now).total_seconds() / 60
|
||||
|
||||
return (
|
||||
f"你当前正在「{activity}」,"
|
||||
f"从{start_time.strftime('%H:%M')}开始,预计{end_time.strftime('%H:%M')}结束,"
|
||||
f"已进行{duration_minutes:.0f}分钟,还剩约{remaining_minutes:.0f}分钟。"
|
||||
)
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
|
||||
return f"你当前正在「{activity}」"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"构建日程块失败: {e}")
|
||||
return ""
|
||||
|
||||
async def _build_time_block(self) -> str:
|
||||
"""构建时间信息块"""
|
||||
now = datetime.now()
|
||||
weekdays = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
|
||||
weekday = weekdays[now.weekday()]
|
||||
|
||||
return f"{now.strftime('%Y年%m月%d日')} {weekday} {now.strftime('%H:%M:%S')}"
|
||||
|
||||
async def build_s4u_style_history(
|
||||
self,
|
||||
context: "StreamContext",
|
||||
max_read: int = 10,
|
||||
max_unread: int = 10,
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
构建 S4U 风格的已读/未读历史消息
|
||||
|
||||
从 S4U 的 build_s4u_chat_history_prompts 移植
|
||||
|
||||
Args:
|
||||
context: 聊天流上下文
|
||||
max_read: 最大已读消息数
|
||||
max_unread: 最大未读消息数
|
||||
|
||||
Returns:
|
||||
tuple[str, str]: (已读历史, 未读历史)
|
||||
"""
|
||||
try:
|
||||
from src.chat.utils.chat_message_builder import build_readable_messages, replace_user_references_async
|
||||
|
||||
# 确保历史消息已初始化
|
||||
await context.ensure_history_initialized()
|
||||
|
||||
read_messages = context.history_messages
|
||||
unread_messages = context.get_unread_messages()
|
||||
|
||||
# 构建已读历史
|
||||
read_history = ""
|
||||
if read_messages:
|
||||
read_dicts = [msg.flatten() for msg in read_messages[-max_read:]]
|
||||
read_content = await build_readable_messages(
|
||||
read_dicts,
|
||||
replace_bot_name=True,
|
||||
timestamp_mode="normal_no_YMD",
|
||||
truncate=True,
|
||||
)
|
||||
read_history = f"### 📜 已读历史消息\n{read_content}"
|
||||
|
||||
# 构建未读历史
|
||||
unread_history = ""
|
||||
if unread_messages:
|
||||
unread_lines = []
|
||||
for msg in unread_messages[-max_unread:]:
|
||||
msg_time = time.strftime("%H:%M:%S", time.localtime(msg.time))
|
||||
msg_content = msg.processed_plain_text or ""
|
||||
|
||||
# 获取发送者名称
|
||||
sender_name = await self._get_sender_name(msg)
|
||||
|
||||
# 处理消息内容中的用户引用
|
||||
if msg_content:
|
||||
msg_content = await replace_user_references_async(
|
||||
msg_content,
|
||||
self.platform,
|
||||
replace_bot_name=True
|
||||
)
|
||||
|
||||
unread_lines.append(f"{msg_time} {sender_name}: {msg_content}")
|
||||
|
||||
unread_history = f"### 📬 未读历史消息\n" + "\n".join(unread_lines)
|
||||
|
||||
return read_history, unread_history
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"构建S4U风格历史失败: {e}")
|
||||
return "", ""
|
||||
|
||||
async def _get_sender_name(self, msg) -> str:
|
||||
"""获取消息发送者名称"""
|
||||
config = _get_config()
|
||||
|
||||
try:
|
||||
user_info = getattr(msg, "user_info", {})
|
||||
platform = getattr(user_info, "platform", "") or getattr(msg, "platform", "")
|
||||
user_id = getattr(user_info, "user_id", "") or getattr(msg, "user_id", "")
|
||||
|
||||
if not (platform and user_id):
|
||||
return "未知用户"
|
||||
|
||||
person_id = PersonInfoManager.get_person_id(platform, user_id)
|
||||
person_info_manager = get_person_info_manager()
|
||||
sender_name = await person_info_manager.get_value(person_id, "person_name") or "未知用户"
|
||||
|
||||
# 如果是Bot自己,标记为(你)
|
||||
if user_id == str(config.bot.qq_account):
|
||||
sender_name = f"{config.bot.nickname}(你)"
|
||||
|
||||
return sender_name
|
||||
|
||||
except Exception:
|
||||
return "未知用户"
|
||||
|
||||
|
||||
# 模块级便捷函数
|
||||
async def build_kfc_context(
|
||||
chat_stream: "ChatStream",
|
||||
sender_name: str,
|
||||
target_message: str,
|
||||
context: Optional["StreamContext"] = None,
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
便捷函数:构建KFC所需的所有上下文
|
||||
|
||||
Args:
|
||||
chat_stream: 聊天流
|
||||
sender_name: 发送者名称
|
||||
target_message: 目标消息
|
||||
context: 聊天流上下文
|
||||
|
||||
Returns:
|
||||
dict: 包含所有上下文块的字典
|
||||
"""
|
||||
builder = KFCContextBuilder(chat_stream)
|
||||
return await builder.build_all_context(sender_name, target_message, context)
|
||||
@@ -1,707 +0,0 @@
|
||||
"""
|
||||
Kokoro Flow Chatter 调度器适配器
|
||||
|
||||
基于项目统一的 UnifiedScheduler 实现 KFC 的定时任务功能。
|
||||
不再自己创建后台循环,而是复用全局调度器的基础设施。
|
||||
|
||||
核心功能:
|
||||
1. 会话等待超时检测(短期)
|
||||
2. 连续思考触发(等待期间的内心活动)
|
||||
3. 主动思考检测(长期沉默后主动发起对话)
|
||||
4. 与 UnifiedScheduler 的集成
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any, Callable, Coroutine, Optional
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import global_config
|
||||
from src.plugin_system.apis.unified_scheduler import (
|
||||
TriggerType,
|
||||
unified_scheduler,
|
||||
)
|
||||
|
||||
from .models import (
|
||||
KokoroSession,
|
||||
MentalLogEntry,
|
||||
MentalLogEventType,
|
||||
SessionStatus,
|
||||
)
|
||||
from .session_manager import get_session_manager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .chatter import KokoroFlowChatter
|
||||
|
||||
logger = get_logger("kokoro_scheduler_adapter")
|
||||
|
||||
|
||||
class KFCSchedulerAdapter:
|
||||
"""
|
||||
KFC 调度器适配器
|
||||
|
||||
使用 UnifiedScheduler 实现 KFC 的定时任务功能,不再自行管理后台循环。
|
||||
|
||||
核心功能:
|
||||
1. 定期检查处于 WAITING 状态的会话(短期等待超时)
|
||||
2. 在特定时间点触发"连续思考"(等待期间内心活动)
|
||||
3. 定期检查长期沉默的会话,触发"主动思考"(长期主动发起)
|
||||
4. 处理等待超时并触发决策
|
||||
"""
|
||||
|
||||
# 连续思考触发点(等待进度的百分比)
|
||||
CONTINUOUS_THINKING_TRIGGERS = [0.3, 0.6, 0.85]
|
||||
|
||||
# 任务名称常量
|
||||
TASK_NAME_WAITING_CHECK = "kfc_waiting_check"
|
||||
TASK_NAME_PROACTIVE_CHECK = "kfc_proactive_check"
|
||||
|
||||
# 主动思考检查间隔(5分钟)
|
||||
PROACTIVE_CHECK_INTERVAL = 300.0
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
check_interval: float = 10.0,
|
||||
on_timeout_callback: Optional[Callable[[KokoroSession], Coroutine[Any, Any, None]]] = None,
|
||||
on_continuous_thinking_callback: Optional[Callable[[KokoroSession], Coroutine[Any, Any, None]]] = None,
|
||||
on_proactive_thinking_callback: Optional[Callable[[KokoroSession, str], Coroutine[Any, Any, None]]] = None,
|
||||
):
|
||||
"""
|
||||
初始化调度器适配器
|
||||
|
||||
Args:
|
||||
check_interval: 等待检查间隔(秒)
|
||||
on_timeout_callback: 超时回调函数
|
||||
on_continuous_thinking_callback: 连续思考回调函数
|
||||
on_proactive_thinking_callback: 主动思考回调函数,接收 (session, trigger_reason)
|
||||
"""
|
||||
self.check_interval = check_interval
|
||||
self.on_timeout_callback = on_timeout_callback
|
||||
self.on_continuous_thinking_callback = on_continuous_thinking_callback
|
||||
self.on_proactive_thinking_callback = on_proactive_thinking_callback
|
||||
|
||||
self._registered = False
|
||||
self._schedule_id: Optional[str] = None
|
||||
self._proactive_schedule_id: Optional[str] = None
|
||||
|
||||
# 加载主动思考配置
|
||||
self._load_proactive_config()
|
||||
|
||||
# 统计信息
|
||||
self._stats = {
|
||||
"total_checks": 0,
|
||||
"timeouts_triggered": 0,
|
||||
"continuous_thinking_triggered": 0,
|
||||
"proactive_thinking_triggered": 0,
|
||||
"proactive_checks": 0,
|
||||
"last_check_time": 0.0,
|
||||
}
|
||||
|
||||
logger.info("KFCSchedulerAdapter 初始化完成")
|
||||
|
||||
def _load_proactive_config(self) -> None:
|
||||
"""加载主动思考相关配置"""
|
||||
try:
|
||||
if global_config and hasattr(global_config, 'kokoro_flow_chatter'):
|
||||
proactive_cfg = global_config.kokoro_flow_chatter.proactive_thinking
|
||||
self.proactive_enabled = proactive_cfg.enabled
|
||||
self.silence_threshold = proactive_cfg.silence_threshold_seconds
|
||||
self.min_interval = proactive_cfg.min_interval_between_proactive
|
||||
self.min_affinity = getattr(proactive_cfg, 'min_affinity_for_proactive', 0.3)
|
||||
self.quiet_hours_start = getattr(proactive_cfg, 'quiet_hours_start', "23:00")
|
||||
self.quiet_hours_end = getattr(proactive_cfg, 'quiet_hours_end', "07:00")
|
||||
else:
|
||||
# 默认值
|
||||
self.proactive_enabled = True
|
||||
self.silence_threshold = 7200 # 2小时
|
||||
self.min_interval = 1800 # 30分钟
|
||||
self.min_affinity = 0.3
|
||||
self.quiet_hours_start = "23:00"
|
||||
self.quiet_hours_end = "07:00"
|
||||
except Exception as e:
|
||||
logger.warning(f"加载主动思考配置失败,使用默认值: {e}")
|
||||
self.proactive_enabled = True
|
||||
self.silence_threshold = 7200
|
||||
self.min_interval = 1800
|
||||
self.min_affinity = 0.3
|
||||
self.quiet_hours_start = "23:00"
|
||||
self.quiet_hours_end = "07:00"
|
||||
|
||||
async def start(self) -> None:
|
||||
"""启动调度器(注册到 UnifiedScheduler)"""
|
||||
if self._registered:
|
||||
logger.warning("KFC 调度器已在运行中")
|
||||
return
|
||||
|
||||
# 注册周期性等待检查任务(每10秒)
|
||||
self._schedule_id = await unified_scheduler.create_schedule(
|
||||
callback=self._check_waiting_sessions,
|
||||
trigger_type=TriggerType.TIME,
|
||||
trigger_config={"delay_seconds": self.check_interval},
|
||||
is_recurring=True,
|
||||
task_name=self.TASK_NAME_WAITING_CHECK,
|
||||
force_overwrite=True,
|
||||
timeout=30.0,
|
||||
)
|
||||
|
||||
# 如果启用了主动思考,注册主动思考检查任务(每5分钟)
|
||||
if self.proactive_enabled:
|
||||
self._proactive_schedule_id = await unified_scheduler.create_schedule(
|
||||
callback=self._check_proactive_sessions,
|
||||
trigger_type=TriggerType.TIME,
|
||||
trigger_config={"delay_seconds": self.PROACTIVE_CHECK_INTERVAL},
|
||||
is_recurring=True,
|
||||
task_name=self.TASK_NAME_PROACTIVE_CHECK,
|
||||
force_overwrite=True,
|
||||
timeout=120.0, # 主动思考可能需要更长时间(涉及 LLM 调用)
|
||||
)
|
||||
logger.info(f"KFC 主动思考调度已注册: schedule_id={self._proactive_schedule_id}")
|
||||
|
||||
self._registered = True
|
||||
logger.info(f"KFC 调度器已注册到 UnifiedScheduler: schedule_id={self._schedule_id}")
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""停止调度器(从 UnifiedScheduler 注销)"""
|
||||
if not self._registered:
|
||||
return
|
||||
|
||||
try:
|
||||
if self._schedule_id:
|
||||
await unified_scheduler.remove_schedule(self._schedule_id)
|
||||
logger.info(f"KFC 等待检查调度已注销: schedule_id={self._schedule_id}")
|
||||
if self._proactive_schedule_id:
|
||||
await unified_scheduler.remove_schedule(self._proactive_schedule_id)
|
||||
logger.info(f"KFC 主动思考调度已注销: schedule_id={self._proactive_schedule_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"停止 KFC 调度器时出错: {e}")
|
||||
finally:
|
||||
self._registered = False
|
||||
self._schedule_id = None
|
||||
self._proactive_schedule_id = None
|
||||
|
||||
async def _check_waiting_sessions(self) -> None:
|
||||
"""检查所有等待中的会话(由 UnifiedScheduler 调用)
|
||||
|
||||
优化:使用 asyncio.create_task 并行处理多个会话,避免顺序阻塞
|
||||
"""
|
||||
session_manager = get_session_manager()
|
||||
waiting_sessions = await session_manager.get_all_waiting_sessions()
|
||||
|
||||
self._stats["total_checks"] += 1
|
||||
self._stats["last_check_time"] = time.time()
|
||||
|
||||
if not waiting_sessions:
|
||||
return
|
||||
|
||||
# 并行处理所有等待中的会话,避免一个会话阻塞其他会话
|
||||
tasks = []
|
||||
for session in waiting_sessions:
|
||||
task = asyncio.create_task(
|
||||
self._safe_process_waiting_session(session),
|
||||
name=f"kfc_session_check_{session.user_id}"
|
||||
)
|
||||
tasks.append(task)
|
||||
|
||||
# 等待所有任务完成,但每个任务都有独立的异常处理
|
||||
if tasks:
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
async def _safe_process_waiting_session(self, session: KokoroSession) -> None:
|
||||
"""安全地处理等待会话,带有超时保护"""
|
||||
try:
|
||||
# 给每个会话处理设置 60 秒超时(LLM 调用可能需要较长时间)
|
||||
await asyncio.wait_for(
|
||||
self._process_waiting_session(session),
|
||||
timeout=60.0
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"处理等待会话 {session.user_id} 超时(60秒)")
|
||||
except Exception as e:
|
||||
logger.error(f"处理等待会话 {session.user_id} 时出错: {e}")
|
||||
|
||||
async def _process_waiting_session(self, session: KokoroSession) -> None:
|
||||
"""
|
||||
处理单个等待中的会话
|
||||
|
||||
Args:
|
||||
session: 等待中的会话
|
||||
"""
|
||||
if session.status != SessionStatus.WAITING:
|
||||
return
|
||||
|
||||
if session.waiting_since is None:
|
||||
return
|
||||
|
||||
wait_duration = session.get_waiting_duration()
|
||||
max_wait = session.max_wait_seconds
|
||||
|
||||
# max_wait_seconds = 0 表示不等待,直接返回 IDLE
|
||||
if max_wait <= 0:
|
||||
logger.info(f"会话 {session.user_id} 设置为不等待 (max_wait=0),返回空闲状态")
|
||||
session.status = SessionStatus.IDLE
|
||||
session.end_waiting()
|
||||
session_manager = get_session_manager()
|
||||
await session_manager.save_session(session.user_id)
|
||||
return
|
||||
|
||||
# 检查是否超时
|
||||
if session.is_wait_timeout():
|
||||
logger.info(f"会话 {session.user_id} 等待超时,触发决策")
|
||||
await self._handle_timeout(session)
|
||||
return
|
||||
|
||||
# 检查是否需要触发连续思考
|
||||
wait_progress = wait_duration / max_wait if max_wait > 0 else 0
|
||||
|
||||
for trigger_point in self.CONTINUOUS_THINKING_TRIGGERS:
|
||||
if self._should_trigger_continuous_thinking(session, wait_progress, trigger_point):
|
||||
logger.debug(
|
||||
f"会话 {session.user_id} 触发连续思考 "
|
||||
f"(进度: {wait_progress:.1%}, 触发点: {trigger_point:.1%})"
|
||||
)
|
||||
await self._handle_continuous_thinking(session, wait_progress)
|
||||
break
|
||||
|
||||
def _should_trigger_continuous_thinking(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
current_progress: float,
|
||||
trigger_point: float,
|
||||
) -> bool:
|
||||
"""
|
||||
判断是否应该触发连续思考
|
||||
"""
|
||||
if current_progress < trigger_point:
|
||||
return False
|
||||
|
||||
expected_count = sum(
|
||||
1 for tp in self.CONTINUOUS_THINKING_TRIGGERS
|
||||
if current_progress >= tp
|
||||
)
|
||||
|
||||
if session.continuous_thinking_count < expected_count:
|
||||
if session.last_continuous_thinking_at is None:
|
||||
return True
|
||||
|
||||
time_since_last = time.time() - session.last_continuous_thinking_at
|
||||
return time_since_last >= 30.0
|
||||
|
||||
return False
|
||||
|
||||
async def _handle_timeout(self, session: KokoroSession) -> None:
|
||||
"""
|
||||
处理等待超时
|
||||
|
||||
Args:
|
||||
session: 超时的会话
|
||||
"""
|
||||
self._stats["timeouts_triggered"] += 1
|
||||
|
||||
# 更新会话状态
|
||||
session.status = SessionStatus.FOLLOW_UP_PENDING
|
||||
session.emotional_state.anxiety_level = 0.8
|
||||
|
||||
# 添加超时日志
|
||||
timeout_entry = MentalLogEntry(
|
||||
event_type=MentalLogEventType.TIMEOUT_DECISION,
|
||||
timestamp=time.time(),
|
||||
thought=f"等了{session.max_wait_seconds}秒了,对方还是没有回复...",
|
||||
content="等待超时",
|
||||
emotional_snapshot=session.emotional_state.to_dict(),
|
||||
)
|
||||
session.add_mental_log_entry(timeout_entry)
|
||||
|
||||
# 保存会话状态
|
||||
session_manager = get_session_manager()
|
||||
await session_manager.save_session(session.user_id)
|
||||
|
||||
# 调用超时回调
|
||||
if self.on_timeout_callback:
|
||||
try:
|
||||
await self.on_timeout_callback(session)
|
||||
except Exception as e:
|
||||
logger.error(f"执行超时回调时出错 (user={session.user_id}): {e}")
|
||||
|
||||
async def _handle_continuous_thinking(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
wait_progress: float,
|
||||
) -> None:
|
||||
"""
|
||||
处理连续思考
|
||||
|
||||
Args:
|
||||
session: 会话
|
||||
wait_progress: 等待进度
|
||||
"""
|
||||
self._stats["continuous_thinking_triggered"] += 1
|
||||
|
||||
# 更新焦虑程度
|
||||
session.emotional_state.update_anxiety_over_time(
|
||||
session.get_waiting_duration(),
|
||||
session.max_wait_seconds
|
||||
)
|
||||
|
||||
# 更新连续思考计数
|
||||
session.continuous_thinking_count += 1
|
||||
session.last_continuous_thinking_at = time.time()
|
||||
|
||||
# 生成基于进度的内心想法
|
||||
thought = self._generate_waiting_thought(session, wait_progress)
|
||||
|
||||
# 添加连续思考日志
|
||||
thinking_entry = MentalLogEntry(
|
||||
event_type=MentalLogEventType.CONTINUOUS_THINKING,
|
||||
timestamp=time.time(),
|
||||
thought=thought,
|
||||
content="",
|
||||
emotional_snapshot=session.emotional_state.to_dict(),
|
||||
metadata={"wait_progress": wait_progress},
|
||||
)
|
||||
session.add_mental_log_entry(thinking_entry)
|
||||
|
||||
# 保存会话状态
|
||||
session_manager = get_session_manager()
|
||||
await session_manager.save_session(session.user_id)
|
||||
|
||||
# 调用连续思考回调
|
||||
if self.on_continuous_thinking_callback:
|
||||
try:
|
||||
await self.on_continuous_thinking_callback(session)
|
||||
except Exception as e:
|
||||
logger.error(f"执行连续思考回调时出错 (user={session.user_id}): {e}")
|
||||
|
||||
def _generate_waiting_thought(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
wait_progress: float,
|
||||
) -> str:
|
||||
"""
|
||||
生成等待中的内心想法(简单版本,不调用LLM)
|
||||
"""
|
||||
import random
|
||||
|
||||
wait_seconds = session.get_waiting_duration()
|
||||
wait_minutes = wait_seconds / 60
|
||||
|
||||
if wait_progress < 0.4:
|
||||
thoughts = [
|
||||
f"已经等了{wait_minutes:.1f}分钟了,对方可能在忙吧...",
|
||||
f"嗯...{wait_minutes:.1f}分钟过去了,不知道对方在做什么",
|
||||
"对方好像还没看到消息,再等等吧",
|
||||
]
|
||||
elif wait_progress < 0.7:
|
||||
thoughts = [
|
||||
f"等了{wait_minutes:.1f}分钟了,有点担心对方是不是不想回了",
|
||||
f"{wait_minutes:.1f}分钟了,对方可能真的很忙?",
|
||||
"时间过得好慢啊...不知道对方什么时候会回复",
|
||||
]
|
||||
else:
|
||||
thoughts = [
|
||||
f"已经等了{wait_minutes:.1f}分钟了,感觉有点焦虑...",
|
||||
f"快{wait_minutes:.0f}分钟了,对方是不是忘记回复了?",
|
||||
"等了这么久,要不要主动说点什么呢...",
|
||||
]
|
||||
|
||||
return random.choice(thoughts)
|
||||
|
||||
# ========================================
|
||||
# 主动思考相关方法(长期沉默后主动发起对话)
|
||||
# ========================================
|
||||
|
||||
async def _check_proactive_sessions(self) -> None:
|
||||
"""
|
||||
检查所有会话是否需要触发主动思考(由 UnifiedScheduler 定期调用)
|
||||
|
||||
主动思考的触发条件:
|
||||
1. 会话处于 IDLE 状态(不在等待回复中)
|
||||
2. 距离上次活动超过 silence_threshold
|
||||
3. 距离上次主动思考超过 min_interval
|
||||
4. 不在勿扰时段
|
||||
5. 与用户的关系亲密度足够
|
||||
"""
|
||||
if not self.proactive_enabled:
|
||||
return
|
||||
|
||||
# 检查是否在勿扰时段
|
||||
if self._is_quiet_hours():
|
||||
logger.debug("[KFC] 当前处于勿扰时段,跳过主动思考检查")
|
||||
return
|
||||
|
||||
self._stats["proactive_checks"] += 1
|
||||
|
||||
session_manager = get_session_manager()
|
||||
all_sessions = await session_manager.get_all_sessions()
|
||||
|
||||
current_time = time.time()
|
||||
|
||||
for session in all_sessions:
|
||||
try:
|
||||
# 检查是否满足主动思考条件(异步获取全局关系分数)
|
||||
trigger_reason = await self._should_trigger_proactive(session, current_time)
|
||||
if trigger_reason:
|
||||
logger.info(
|
||||
f"[KFC] 触发主动思考: user={session.user_id}, reason={trigger_reason}"
|
||||
)
|
||||
await self._handle_proactive_thinking(session, trigger_reason)
|
||||
except Exception as e:
|
||||
logger.error(f"检查主动思考条件时出错 (user={session.user_id}): {e}")
|
||||
|
||||
def _is_quiet_hours(self) -> bool:
|
||||
"""
|
||||
检查当前是否处于勿扰时段
|
||||
|
||||
支持跨午夜的时段(如 23:00 到 07:00)
|
||||
"""
|
||||
try:
|
||||
now = datetime.now()
|
||||
current_minutes = now.hour * 60 + now.minute
|
||||
|
||||
# 解析开始时间
|
||||
start_parts = self.quiet_hours_start.split(":")
|
||||
start_minutes = int(start_parts[0]) * 60 + int(start_parts[1])
|
||||
|
||||
# 解析结束时间
|
||||
end_parts = self.quiet_hours_end.split(":")
|
||||
end_minutes = int(end_parts[0]) * 60 + int(end_parts[1])
|
||||
|
||||
# 处理跨午夜的情况
|
||||
if start_minutes <= end_minutes:
|
||||
# 不跨午夜(如 09:00 到 17:00)
|
||||
return start_minutes <= current_minutes < end_minutes
|
||||
else:
|
||||
# 跨午夜(如 23:00 到 07:00)
|
||||
return current_minutes >= start_minutes or current_minutes < end_minutes
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"解析勿扰时段配置失败: {e}")
|
||||
return False
|
||||
|
||||
async def _should_trigger_proactive(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
current_time: float
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
检查是否应该触发主动思考
|
||||
|
||||
使用全局关系数据库中的关系分数(而不是 KFC 内部的 emotional_state)
|
||||
|
||||
概率机制:关系越亲密,触发概率越高
|
||||
- 亲密度 0.3 → 触发概率 10%
|
||||
- 亲密度 0.5 → 触发概率 30%
|
||||
- 亲密度 0.7 → 触发概率 55%
|
||||
- 亲密度 1.0 → 触发概率 90%
|
||||
|
||||
Args:
|
||||
session: 会话
|
||||
current_time: 当前时间戳
|
||||
|
||||
Returns:
|
||||
触发原因字符串,如果不触发则返回 None
|
||||
"""
|
||||
import random
|
||||
|
||||
# 条件1:必须处于 IDLE 状态
|
||||
if session.status != SessionStatus.IDLE:
|
||||
return None
|
||||
|
||||
# 条件2:距离上次活动超过沉默阈值
|
||||
silence_duration = current_time - session.last_activity_at
|
||||
if silence_duration < self.silence_threshold:
|
||||
return None
|
||||
|
||||
# 条件3:距离上次主动思考超过最小间隔
|
||||
if session.last_proactive_at is not None:
|
||||
time_since_last_proactive = current_time - session.last_proactive_at
|
||||
if time_since_last_proactive < self.min_interval:
|
||||
return None
|
||||
|
||||
# 条件4:从数据库获取全局关系分数
|
||||
relationship_score = await self._get_global_relationship_score(session.user_id)
|
||||
if relationship_score < self.min_affinity:
|
||||
logger.debug(
|
||||
f"主动思考跳过(关系分数不足): user={session.user_id}, "
|
||||
f"score={relationship_score:.2f}, min={self.min_affinity:.2f}"
|
||||
)
|
||||
return None
|
||||
|
||||
# 条件5:基于关系分数的概率判断
|
||||
# 公式:probability = 0.1 + 0.8 * ((score - min_affinity) / (1.0 - min_affinity))^1.5
|
||||
# 这样分数从 min_affinity 到 1.0 映射到概率 10% 到 90%
|
||||
# 使用1.5次幂让曲线更陡峭,高亲密度时概率增长更快
|
||||
normalized_score = (relationship_score - self.min_affinity) / (1.0 - self.min_affinity)
|
||||
probability = 0.1 + 0.8 * (normalized_score ** 1.5)
|
||||
probability = min(probability, 0.9) # 最高90%,永远不是100%确定
|
||||
|
||||
if random.random() > probability:
|
||||
# 这次检查没触发,但记录一下(用于调试)
|
||||
logger.debug(
|
||||
f"主动思考概率检查未通过: user={session.user_id}, "
|
||||
f"score={relationship_score:.2f}, probability={probability:.1%}"
|
||||
)
|
||||
return None
|
||||
|
||||
# 所有条件满足,生成触发原因
|
||||
silence_hours = silence_duration / 3600
|
||||
logger.info(
|
||||
f"主动思考触发: user={session.user_id}, "
|
||||
f"silence={silence_hours:.1f}h, score={relationship_score:.2f}, prob={probability:.1%}"
|
||||
)
|
||||
return f"沉默了{silence_hours:.1f}小时,想主动关心一下对方"
|
||||
|
||||
async def _get_global_relationship_score(self, user_id: str) -> float:
|
||||
"""
|
||||
从全局关系数据库获取关系分数
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
关系分数 (0.0-1.0),如果没有记录返回默认值 0.3
|
||||
"""
|
||||
try:
|
||||
from src.common.database.api.specialized import get_user_relationship
|
||||
|
||||
# 从 user_id 解析 platform(格式通常是 "platform_userid")
|
||||
# 这里假设 user_id 中包含 platform 信息,需要根据实际情况调整
|
||||
# 先尝试直接查询,如果失败再用默认值
|
||||
relationship = await get_user_relationship(
|
||||
platform="qq", # TODO: 从 session 或 stream_id 获取真实 platform
|
||||
user_id=user_id,
|
||||
target_id="bot",
|
||||
)
|
||||
|
||||
if relationship and hasattr(relationship, 'relationship_score'):
|
||||
return relationship.relationship_score
|
||||
|
||||
# 没有找到关系记录,返回默认值
|
||||
return 0.3
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"获取全局关系分数失败 (user={user_id}): {e}")
|
||||
return 0.3 # 出错时返回较低的默认值
|
||||
|
||||
async def _handle_proactive_thinking(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
trigger_reason: str
|
||||
) -> None:
|
||||
"""
|
||||
处理主动思考
|
||||
|
||||
Args:
|
||||
session: 会话
|
||||
trigger_reason: 触发原因
|
||||
"""
|
||||
self._stats["proactive_thinking_triggered"] += 1
|
||||
|
||||
# 更新会话状态
|
||||
session.last_proactive_at = time.time()
|
||||
session.proactive_count += 1
|
||||
|
||||
# 添加主动思考日志
|
||||
proactive_entry = MentalLogEntry(
|
||||
event_type=MentalLogEventType.PROACTIVE_THINKING,
|
||||
timestamp=time.time(),
|
||||
thought=trigger_reason,
|
||||
content="主动思考触发",
|
||||
emotional_snapshot=session.emotional_state.to_dict(),
|
||||
metadata={"trigger_reason": trigger_reason},
|
||||
)
|
||||
session.add_mental_log_entry(proactive_entry)
|
||||
|
||||
# 保存会话状态
|
||||
session_manager = get_session_manager()
|
||||
await session_manager.save_session(session.user_id)
|
||||
|
||||
# 调用主动思考回调(由 chatter 处理实际的 LLM 调用和动作执行)
|
||||
if self.on_proactive_thinking_callback:
|
||||
try:
|
||||
await self.on_proactive_thinking_callback(session, trigger_reason)
|
||||
except Exception as e:
|
||||
logger.error(f"执行主动思考回调时出错 (user={session.user_id}): {e}")
|
||||
|
||||
def set_timeout_callback(
|
||||
self,
|
||||
callback: Callable[[KokoroSession], Coroutine[Any, Any, None]],
|
||||
) -> None:
|
||||
"""设置超时回调函数"""
|
||||
self.on_timeout_callback = callback
|
||||
|
||||
def set_continuous_thinking_callback(
|
||||
self,
|
||||
callback: Callable[[KokoroSession], Coroutine[Any, Any, None]],
|
||||
) -> None:
|
||||
"""设置连续思考回调函数"""
|
||||
self.on_continuous_thinking_callback = callback
|
||||
|
||||
def set_proactive_thinking_callback(
|
||||
self,
|
||||
callback: Callable[[KokoroSession, str], Coroutine[Any, Any, None]],
|
||||
) -> None:
|
||||
"""设置主动思考回调函数"""
|
||||
self.on_proactive_thinking_callback = callback
|
||||
|
||||
def get_stats(self) -> dict[str, Any]:
|
||||
"""获取统计信息"""
|
||||
return {
|
||||
**self._stats,
|
||||
"is_running": self._registered,
|
||||
"check_interval": self.check_interval,
|
||||
}
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
"""调度器是否正在运行"""
|
||||
return self._registered
|
||||
|
||||
|
||||
# 全局适配器实例
|
||||
_scheduler_adapter: Optional[KFCSchedulerAdapter] = None
|
||||
|
||||
|
||||
def get_scheduler() -> KFCSchedulerAdapter:
|
||||
"""获取全局调度器适配器实例"""
|
||||
global _scheduler_adapter
|
||||
if _scheduler_adapter is None:
|
||||
_scheduler_adapter = KFCSchedulerAdapter()
|
||||
return _scheduler_adapter
|
||||
|
||||
|
||||
async def initialize_scheduler(
|
||||
check_interval: float = 10.0,
|
||||
on_timeout_callback: Optional[Callable[[KokoroSession], Coroutine[Any, Any, None]]] = None,
|
||||
on_continuous_thinking_callback: Optional[Callable[[KokoroSession], Coroutine[Any, Any, None]]] = None,
|
||||
on_proactive_thinking_callback: Optional[Callable[[KokoroSession, str], Coroutine[Any, Any, None]]] = None,
|
||||
) -> KFCSchedulerAdapter:
|
||||
"""
|
||||
初始化并启动调度器
|
||||
|
||||
Args:
|
||||
check_interval: 检查间隔
|
||||
on_timeout_callback: 超时回调
|
||||
on_continuous_thinking_callback: 连续思考回调
|
||||
on_proactive_thinking_callback: 主动思考回调
|
||||
|
||||
Returns:
|
||||
KFCSchedulerAdapter: 调度器适配器实例
|
||||
"""
|
||||
global _scheduler_adapter
|
||||
_scheduler_adapter = KFCSchedulerAdapter(
|
||||
check_interval=check_interval,
|
||||
on_timeout_callback=on_timeout_callback,
|
||||
on_continuous_thinking_callback=on_continuous_thinking_callback,
|
||||
on_proactive_thinking_callback=on_proactive_thinking_callback,
|
||||
)
|
||||
await _scheduler_adapter.start()
|
||||
return _scheduler_adapter
|
||||
|
||||
|
||||
async def shutdown_scheduler() -> None:
|
||||
"""关闭调度器"""
|
||||
global _scheduler_adapter
|
||||
if _scheduler_adapter:
|
||||
await _scheduler_adapter.stop()
|
||||
_scheduler_adapter = None
|
||||
@@ -1,459 +0,0 @@
|
||||
"""
|
||||
Kokoro Flow Chatter 数据模型
|
||||
|
||||
定义心流聊天器的核心数据结构,包括:
|
||||
- SessionStatus: 会话状态枚举
|
||||
- EmotionalState: 情感状态模型
|
||||
- MentalLogEntry: 心理活动日志条目
|
||||
- KokoroSession: 完整的会话模型
|
||||
- LLMResponseModel: LLM响应结构
|
||||
- ActionModel: 动作模型
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, Optional
|
||||
import time
|
||||
|
||||
|
||||
class SessionStatus(Enum):
|
||||
"""
|
||||
会话状态枚举
|
||||
|
||||
状态机核心,定义了KFC系统的四个基本状态:
|
||||
- IDLE: 空闲态,会话的起点和终点
|
||||
- RESPONDING: 响应中,正在处理消息和生成决策
|
||||
- WAITING: 等待态,已发送回复,等待用户回应
|
||||
- FOLLOW_UP_PENDING: 决策态,等待超时后进行后续决策
|
||||
"""
|
||||
IDLE = "idle"
|
||||
RESPONDING = "responding"
|
||||
WAITING = "waiting"
|
||||
FOLLOW_UP_PENDING = "follow_up_pending"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
|
||||
class MentalLogEventType(Enum):
|
||||
"""
|
||||
心理活动日志事件类型
|
||||
|
||||
用于标记线性叙事历史中不同类型的事件
|
||||
"""
|
||||
USER_MESSAGE = "user_message" # 用户消息事件
|
||||
BOT_ACTION = "bot_action" # Bot行动事件
|
||||
WAITING_UPDATE = "waiting_update" # 等待期间的心理更新
|
||||
TIMEOUT_DECISION = "timeout_decision" # 超时决策事件
|
||||
STATE_CHANGE = "state_change" # 状态变更事件
|
||||
CONTINUOUS_THINKING = "continuous_thinking" # 连续思考事件
|
||||
PROACTIVE_THINKING = "proactive_thinking" # 主动思考事件(长期沉默后主动发起)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
|
||||
@dataclass
|
||||
class EmotionalState:
|
||||
"""
|
||||
动态情感状态模型
|
||||
|
||||
记录和跟踪AI的情感参数,用于驱动个性化的交互行为
|
||||
|
||||
Attributes:
|
||||
mood: 当前心情标签(如:开心、好奇、疲惫、沮丧)
|
||||
mood_intensity: 心情强度,0.0-1.0
|
||||
relationship_warmth: 关系热度,代表与用户的亲密度,0.0-1.0
|
||||
impression_of_user: 对用户的动态印象描述
|
||||
anxiety_level: 焦虑程度,0.0-1.0,在等待时会变化
|
||||
engagement_level: 投入程度,0.0-1.0,表示对当前对话的关注度
|
||||
last_update_time: 最后更新时间戳
|
||||
"""
|
||||
mood: str = "平静" # V7: 改为中文"平静",更自然
|
||||
mood_intensity: float = 0.3 # V7: 默认低强度,避免无厘头的强烈情绪
|
||||
relationship_warmth: float = 0.5
|
||||
impression_of_user: str = ""
|
||||
anxiety_level: float = 0.0
|
||||
engagement_level: float = 0.5
|
||||
last_update_time: float = field(default_factory=time.time)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""转换为字典格式"""
|
||||
return {
|
||||
"mood": self.mood,
|
||||
"mood_intensity": self.mood_intensity,
|
||||
"relationship_warmth": self.relationship_warmth,
|
||||
"impression_of_user": self.impression_of_user,
|
||||
"anxiety_level": self.anxiety_level,
|
||||
"engagement_level": self.engagement_level,
|
||||
"last_update_time": self.last_update_time,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "EmotionalState":
|
||||
"""从字典创建实例"""
|
||||
return cls(
|
||||
mood=data.get("mood", "neutral"),
|
||||
mood_intensity=data.get("mood_intensity", 0.5),
|
||||
relationship_warmth=data.get("relationship_warmth", 0.5),
|
||||
impression_of_user=data.get("impression_of_user", ""),
|
||||
anxiety_level=data.get("anxiety_level", 0.0),
|
||||
engagement_level=data.get("engagement_level", 0.5),
|
||||
last_update_time=data.get("last_update_time", time.time()),
|
||||
)
|
||||
|
||||
def update_anxiety_over_time(self, elapsed_seconds: float, max_wait_seconds: float) -> None:
|
||||
"""
|
||||
根据等待时间更新焦虑程度
|
||||
|
||||
Args:
|
||||
elapsed_seconds: 已等待的秒数
|
||||
max_wait_seconds: 最大等待秒数
|
||||
"""
|
||||
if max_wait_seconds <= 0:
|
||||
return
|
||||
|
||||
# 焦虑程度随时间流逝增加,使用平方根函数使增长趋于平缓
|
||||
wait_ratio = min(elapsed_seconds / max_wait_seconds, 1.0)
|
||||
self.anxiety_level = min(wait_ratio ** 0.5, 1.0)
|
||||
self.last_update_time = time.time()
|
||||
|
||||
|
||||
@dataclass
|
||||
class MentalLogEntry:
|
||||
"""
|
||||
心理活动日志条目
|
||||
|
||||
记录线性叙事历史中的每一个事件节点,
|
||||
是实现"连续主观体验"的核心数据结构
|
||||
|
||||
Attributes:
|
||||
event_type: 事件类型
|
||||
timestamp: 事件发生时间戳
|
||||
thought: 内心独白
|
||||
content: 事件内容(如用户消息、Bot回复等)
|
||||
emotional_snapshot: 事件发生时的情感状态快照
|
||||
metadata: 额外元数据
|
||||
"""
|
||||
event_type: MentalLogEventType
|
||||
timestamp: float
|
||||
thought: str = ""
|
||||
content: str = ""
|
||||
emotional_snapshot: Optional[dict[str, Any]] = None
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""转换为字典格式"""
|
||||
return {
|
||||
"event_type": str(self.event_type),
|
||||
"timestamp": self.timestamp,
|
||||
"thought": self.thought,
|
||||
"content": self.content,
|
||||
"emotional_snapshot": self.emotional_snapshot,
|
||||
"metadata": self.metadata,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "MentalLogEntry":
|
||||
"""从字典创建实例"""
|
||||
event_type_str = data.get("event_type", "state_change")
|
||||
try:
|
||||
event_type = MentalLogEventType(event_type_str)
|
||||
except ValueError:
|
||||
event_type = MentalLogEventType.STATE_CHANGE
|
||||
|
||||
return cls(
|
||||
event_type=event_type,
|
||||
timestamp=data.get("timestamp", time.time()),
|
||||
thought=data.get("thought", ""),
|
||||
content=data.get("content", ""),
|
||||
emotional_snapshot=data.get("emotional_snapshot"),
|
||||
metadata=data.get("metadata", {}),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class KokoroSession:
|
||||
"""
|
||||
Kokoro Flow Chatter 会话模型
|
||||
|
||||
为每个私聊用户维护一个独立的会话,包含:
|
||||
- 基本会话信息
|
||||
- 当前状态
|
||||
- 情感状态
|
||||
- 线性叙事历史(心理活动日志)
|
||||
- 等待相关的状态
|
||||
|
||||
Attributes:
|
||||
user_id: 用户唯一标识
|
||||
stream_id: 聊天流ID
|
||||
status: 当前会话状态
|
||||
emotional_state: 动态情感状态
|
||||
mental_log: 线性叙事历史
|
||||
expected_user_reaction: 对用户回应的预期
|
||||
max_wait_seconds: 最大等待秒数
|
||||
waiting_since: 开始等待的时间戳
|
||||
last_bot_message: 最后一条Bot消息
|
||||
last_user_message: 最后一条用户消息
|
||||
created_at: 会话创建时间
|
||||
last_activity_at: 最后活动时间
|
||||
total_interactions: 总交互次数
|
||||
"""
|
||||
user_id: str
|
||||
stream_id: str
|
||||
status: SessionStatus = SessionStatus.IDLE
|
||||
emotional_state: EmotionalState = field(default_factory=EmotionalState)
|
||||
mental_log: list[MentalLogEntry] = field(default_factory=list)
|
||||
|
||||
# 等待状态相关
|
||||
expected_user_reaction: str = ""
|
||||
max_wait_seconds: int = 300
|
||||
waiting_since: Optional[float] = None
|
||||
|
||||
# 消息记录
|
||||
last_bot_message: str = ""
|
||||
last_user_message: str = ""
|
||||
|
||||
# 统计信息
|
||||
created_at: float = field(default_factory=time.time)
|
||||
last_activity_at: float = field(default_factory=time.time)
|
||||
total_interactions: int = 0
|
||||
|
||||
# 连续思考相关
|
||||
continuous_thinking_count: int = 0
|
||||
last_continuous_thinking_at: Optional[float] = None
|
||||
|
||||
# 主动思考相关(长期沉默后主动发起对话)
|
||||
last_proactive_at: Optional[float] = None # 上次主动思考的时间
|
||||
proactive_count: int = 0 # 主动思考的次数(累计)
|
||||
|
||||
# V7: 连续等待追问限制(防止用户不回复时连续追问)
|
||||
consecutive_followup_count: int = 0 # 用户没回复时连续追问的次数
|
||||
max_consecutive_followups: int = 2 # 最多允许连续追问2次
|
||||
|
||||
def add_mental_log_entry(self, entry: MentalLogEntry, max_log_size: int = 100) -> None:
|
||||
"""
|
||||
添加心理活动日志条目
|
||||
|
||||
Args:
|
||||
entry: 日志条目
|
||||
max_log_size: 日志最大保留条数
|
||||
"""
|
||||
self.mental_log.append(entry)
|
||||
self.last_activity_at = time.time()
|
||||
|
||||
# 保持日志在合理大小
|
||||
if len(self.mental_log) > max_log_size:
|
||||
# 保留最近的日志
|
||||
self.mental_log = self.mental_log[-max_log_size:]
|
||||
|
||||
def get_recent_mental_log(self, limit: int = 20) -> list[MentalLogEntry]:
|
||||
"""获取最近的心理活动日志"""
|
||||
return self.mental_log[-limit:] if self.mental_log else []
|
||||
|
||||
def get_waiting_duration(self) -> float:
|
||||
"""获取当前等待时长(秒)"""
|
||||
if self.waiting_since is None:
|
||||
return 0.0
|
||||
return time.time() - self.waiting_since
|
||||
|
||||
def is_wait_timeout(self) -> bool:
|
||||
"""检查是否等待超时"""
|
||||
return self.get_waiting_duration() >= self.max_wait_seconds
|
||||
|
||||
def start_waiting(self, expected_reaction: str, max_wait: int) -> None:
|
||||
"""开始等待状态"""
|
||||
self.status = SessionStatus.WAITING
|
||||
self.expected_user_reaction = expected_reaction
|
||||
self.max_wait_seconds = max_wait
|
||||
self.waiting_since = time.time()
|
||||
self.continuous_thinking_count = 0
|
||||
|
||||
def end_waiting(self) -> None:
|
||||
"""结束等待状态"""
|
||||
self.waiting_since = None
|
||||
self.expected_user_reaction = ""
|
||||
self.continuous_thinking_count = 0
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""转换为可序列化的字典格式"""
|
||||
return {
|
||||
"user_id": self.user_id,
|
||||
"stream_id": self.stream_id,
|
||||
"status": str(self.status),
|
||||
"emotional_state": self.emotional_state.to_dict(),
|
||||
"mental_log": [entry.to_dict() for entry in self.mental_log],
|
||||
"expected_user_reaction": self.expected_user_reaction,
|
||||
"max_wait_seconds": self.max_wait_seconds,
|
||||
"waiting_since": self.waiting_since,
|
||||
"last_bot_message": self.last_bot_message,
|
||||
"last_user_message": self.last_user_message,
|
||||
"created_at": self.created_at,
|
||||
"last_activity_at": self.last_activity_at,
|
||||
"total_interactions": self.total_interactions,
|
||||
"continuous_thinking_count": self.continuous_thinking_count,
|
||||
"last_continuous_thinking_at": self.last_continuous_thinking_at,
|
||||
"last_proactive_at": self.last_proactive_at,
|
||||
"proactive_count": self.proactive_count,
|
||||
"consecutive_followup_count": self.consecutive_followup_count,
|
||||
"max_consecutive_followups": self.max_consecutive_followups,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "KokoroSession":
|
||||
"""从字典创建会话实例"""
|
||||
status_str = data.get("status", "idle")
|
||||
try:
|
||||
status = SessionStatus(status_str)
|
||||
except ValueError:
|
||||
status = SessionStatus.IDLE
|
||||
|
||||
emotional_state = EmotionalState.from_dict(
|
||||
data.get("emotional_state", {})
|
||||
)
|
||||
|
||||
mental_log = [
|
||||
MentalLogEntry.from_dict(entry)
|
||||
for entry in data.get("mental_log", [])
|
||||
]
|
||||
|
||||
return cls(
|
||||
user_id=data.get("user_id", ""),
|
||||
stream_id=data.get("stream_id", ""),
|
||||
status=status,
|
||||
emotional_state=emotional_state,
|
||||
mental_log=mental_log,
|
||||
expected_user_reaction=data.get("expected_user_reaction", ""),
|
||||
max_wait_seconds=data.get("max_wait_seconds", 300),
|
||||
waiting_since=data.get("waiting_since"),
|
||||
last_bot_message=data.get("last_bot_message", ""),
|
||||
last_user_message=data.get("last_user_message", ""),
|
||||
created_at=data.get("created_at", time.time()),
|
||||
last_activity_at=data.get("last_activity_at", time.time()),
|
||||
total_interactions=data.get("total_interactions", 0),
|
||||
continuous_thinking_count=data.get("continuous_thinking_count", 0),
|
||||
last_continuous_thinking_at=data.get("last_continuous_thinking_at"),
|
||||
last_proactive_at=data.get("last_proactive_at"),
|
||||
proactive_count=data.get("proactive_count", 0),
|
||||
consecutive_followup_count=data.get("consecutive_followup_count", 0),
|
||||
max_consecutive_followups=data.get("max_consecutive_followups", 2),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ActionModel:
|
||||
"""
|
||||
动作模型
|
||||
|
||||
表示LLM决策的单个动作
|
||||
|
||||
Attributes:
|
||||
type: 动作类型(reply, poke_user, send_reaction, update_internal_state, do_nothing)
|
||||
params: 动作参数
|
||||
"""
|
||||
type: str
|
||||
params: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""转换为字典格式"""
|
||||
return {
|
||||
"type": self.type,
|
||||
**self.params
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "ActionModel":
|
||||
"""从字典创建实例"""
|
||||
action_type = data.get("type", "do_nothing")
|
||||
params = {k: v for k, v in data.items() if k != "type"}
|
||||
return cls(type=action_type, params=params)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMResponseModel:
|
||||
"""
|
||||
LLM响应模型
|
||||
|
||||
定义LLM输出的结构化JSON格式
|
||||
|
||||
Attributes:
|
||||
thought: 内心独白(必须)
|
||||
expected_user_reaction: 用户回应预期(必须)
|
||||
max_wait_seconds: 最长等待秒数(必须)
|
||||
actions: 行动列表(必须)
|
||||
plan: 行动意图(可选)
|
||||
emotional_updates: 情感状态更新(可选)
|
||||
"""
|
||||
thought: str
|
||||
expected_user_reaction: str
|
||||
max_wait_seconds: int
|
||||
actions: list[ActionModel]
|
||||
plan: str = ""
|
||||
emotional_updates: Optional[dict[str, Any]] = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""转换为字典格式"""
|
||||
result = {
|
||||
"thought": self.thought,
|
||||
"expected_user_reaction": self.expected_user_reaction,
|
||||
"max_wait_seconds": self.max_wait_seconds,
|
||||
"actions": [action.to_dict() for action in self.actions],
|
||||
}
|
||||
if self.plan:
|
||||
result["plan"] = self.plan
|
||||
if self.emotional_updates:
|
||||
result["emotional_updates"] = self.emotional_updates
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "LLMResponseModel":
|
||||
"""从字典创建实例"""
|
||||
actions = [
|
||||
ActionModel.from_dict(action)
|
||||
for action in data.get("actions", [])
|
||||
]
|
||||
|
||||
# 如果没有actions,添加默认的do_nothing
|
||||
if not actions:
|
||||
actions = [ActionModel(type="do_nothing")]
|
||||
|
||||
return cls(
|
||||
thought=data.get("thought", ""),
|
||||
expected_user_reaction=data.get("expected_user_reaction", ""),
|
||||
max_wait_seconds=data.get("max_wait_seconds", 300),
|
||||
actions=actions,
|
||||
plan=data.get("plan", ""),
|
||||
emotional_updates=data.get("emotional_updates"),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create_error_response(cls, error_message: str) -> "LLMResponseModel":
|
||||
"""创建错误响应"""
|
||||
return cls(
|
||||
thought=f"出现了问题:{error_message}",
|
||||
expected_user_reaction="用户可能会感到困惑",
|
||||
max_wait_seconds=60,
|
||||
actions=[ActionModel(type="do_nothing")],
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContinuousThinkingResult:
|
||||
"""
|
||||
连续思考结果
|
||||
|
||||
在等待期间触发的心理活动更新结果
|
||||
"""
|
||||
thought: str
|
||||
anxiety_level: float
|
||||
should_follow_up: bool = False
|
||||
follow_up_message: str = ""
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""转换为字典格式"""
|
||||
return {
|
||||
"thought": self.thought,
|
||||
"anxiety_level": self.anxiety_level,
|
||||
"should_follow_up": self.should_follow_up,
|
||||
"follow_up_message": self.follow_up_message,
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
"""
|
||||
Kokoro Flow Chatter (心流聊天器) 插件入口
|
||||
|
||||
这是一个专为私聊场景设计的AI聊天插件,实现从"消息响应者"到"对话体验者"的转变。
|
||||
|
||||
核心特点:
|
||||
- 心理状态驱动的交互模型
|
||||
- 连续的时间观念和等待体验
|
||||
- 深度情感连接和长期关系维护
|
||||
- 状态机驱动的交互节奏
|
||||
|
||||
切换逻辑:
|
||||
- 当 enable = true 时,KFC 接管所有私聊消息
|
||||
- 当 enable = false 时,私聊消息由 AFC (Affinity Flow Chatter) 处理
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import global_config
|
||||
from src.plugin_system.apis.plugin_register_api import register_plugin
|
||||
from src.plugin_system.base.base_plugin import BasePlugin
|
||||
from src.plugin_system.base.component_types import ComponentInfo
|
||||
|
||||
logger = get_logger("kokoro_flow_chatter_plugin")
|
||||
|
||||
|
||||
@register_plugin
|
||||
class KokoroFlowChatterPlugin(BasePlugin):
|
||||
"""
|
||||
心流聊天器插件
|
||||
|
||||
专为私聊场景设计的深度情感交互处理器。
|
||||
|
||||
Features:
|
||||
- KokoroFlowChatter: 核心聊天处理器组件
|
||||
- SessionManager: 会话管理,支持持久化
|
||||
- BackgroundScheduler: 后台调度,处理等待超时
|
||||
- PromptGenerator: 动态提示词生成
|
||||
- ActionExecutor: 动作解析和执行
|
||||
"""
|
||||
|
||||
plugin_name: str = "kokoro_flow_chatter"
|
||||
enable_plugin: bool = True
|
||||
dependencies: ClassVar[list[str]] = []
|
||||
python_dependencies: ClassVar[list[str]] = []
|
||||
config_file_name: str = "config.toml"
|
||||
|
||||
# 配置schema留空,使用config.toml直接配置
|
||||
config_schema: ClassVar[dict[str, Any]] = {}
|
||||
|
||||
# 后台任务
|
||||
_session_manager = None
|
||||
_scheduler = None
|
||||
_initialization_task = None
|
||||
|
||||
def get_plugin_components(self) -> list[tuple[ComponentInfo, type]]:
|
||||
"""
|
||||
返回插件包含的组件列表
|
||||
|
||||
根据 global_config.kokoro_flow_chatter.enable 决定是否注册 KFC。
|
||||
如果 enable = false,返回空列表,私聊将由 AFC 处理。
|
||||
"""
|
||||
components: list[tuple[ComponentInfo, type]] = []
|
||||
|
||||
# 检查是否启用 KFC
|
||||
kfc_enabled = True
|
||||
if global_config and hasattr(global_config, 'kokoro_flow_chatter'):
|
||||
kfc_enabled = global_config.kokoro_flow_chatter.enable
|
||||
|
||||
if not kfc_enabled:
|
||||
logger.info("KFC 已禁用 (enable = false),私聊将由 AFC 处理")
|
||||
return components
|
||||
|
||||
try:
|
||||
# 导入核心聊天处理器
|
||||
from .chatter import KokoroFlowChatter
|
||||
|
||||
components.append((
|
||||
KokoroFlowChatter.get_chatter_info(),
|
||||
KokoroFlowChatter
|
||||
))
|
||||
logger.debug("成功加载 KokoroFlowChatter 组件,KFC 将接管私聊")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"加载 KokoroFlowChatter 时出错: {e}")
|
||||
|
||||
return components
|
||||
|
||||
async def on_plugin_load(self) -> bool:
|
||||
"""
|
||||
插件加载时的初始化逻辑
|
||||
|
||||
如果 KFC 被禁用,跳过初始化。
|
||||
|
||||
Returns:
|
||||
bool: 是否加载成功
|
||||
"""
|
||||
# 检查是否启用 KFC
|
||||
kfc_enabled = True
|
||||
if global_config and hasattr(global_config, 'kokoro_flow_chatter'):
|
||||
kfc_enabled = global_config.kokoro_flow_chatter.enable
|
||||
|
||||
if not kfc_enabled:
|
||||
logger.info("KFC 已禁用,跳过初始化")
|
||||
self._is_started = False
|
||||
return True
|
||||
|
||||
try:
|
||||
logger.info("正在初始化 Kokoro Flow Chatter 插件...")
|
||||
|
||||
# 初始化会话管理器
|
||||
from .session_manager import initialize_session_manager
|
||||
|
||||
session_config = self.config.get("kokoro_flow_chatter", {}).get("session", {})
|
||||
self._session_manager = await initialize_session_manager(
|
||||
data_dir=session_config.get("data_dir", "data/kokoro_flow_chatter/sessions"),
|
||||
max_session_age_days=session_config.get("max_session_age_days", 30),
|
||||
auto_save_interval=session_config.get("auto_save_interval", 300),
|
||||
)
|
||||
|
||||
# 初始化调度器
|
||||
from .kfc_scheduler_adapter import initialize_scheduler
|
||||
|
||||
# 从 global_config 读取配置
|
||||
check_interval = 10.0
|
||||
if global_config and hasattr(global_config, 'kokoro_flow_chatter'):
|
||||
# 使用简化后的配置结构
|
||||
pass # check_interval 保持默认值
|
||||
|
||||
self._scheduler = await initialize_scheduler(
|
||||
check_interval=check_interval,
|
||||
)
|
||||
|
||||
self._is_started = True
|
||||
logger.info("Kokoro Flow Chatter 插件初始化完成")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Kokoro Flow Chatter 插件初始化失败: {e}")
|
||||
return False
|
||||
|
||||
async def on_plugin_unload(self) -> bool:
|
||||
"""
|
||||
插件卸载时的清理逻辑
|
||||
|
||||
Returns:
|
||||
bool: 是否卸载成功
|
||||
"""
|
||||
try:
|
||||
logger.info("正在关闭 Kokoro Flow Chatter 插件...")
|
||||
|
||||
# 停止调度器
|
||||
if self._scheduler:
|
||||
from .kfc_scheduler_adapter import shutdown_scheduler
|
||||
await shutdown_scheduler()
|
||||
self._scheduler = None
|
||||
|
||||
# 停止会话管理器
|
||||
if self._session_manager:
|
||||
await self._session_manager.stop()
|
||||
self._session_manager = None
|
||||
|
||||
self._is_started = False
|
||||
logger.info("Kokoro Flow Chatter 插件已关闭")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Kokoro Flow Chatter 插件关闭失败: {e}")
|
||||
return False
|
||||
|
||||
def register_plugin(self) -> bool:
|
||||
"""
|
||||
注册插件及其所有组件
|
||||
|
||||
重写父类方法,添加异步初始化逻辑
|
||||
"""
|
||||
# 先调用父类的注册逻辑
|
||||
result = super().register_plugin()
|
||||
|
||||
if result:
|
||||
# 在后台启动异步初始化
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_running():
|
||||
self._initialization_task = asyncio.create_task(
|
||||
self.on_plugin_load()
|
||||
)
|
||||
else:
|
||||
# 如果事件循环未运行,稍后初始化
|
||||
logger.debug("事件循环未运行,将延迟初始化")
|
||||
except RuntimeError:
|
||||
logger.debug("无法获取事件循环,将延迟初始化")
|
||||
|
||||
return result
|
||||
|
||||
@property
|
||||
def is_started(self) -> bool:
|
||||
"""插件是否已启动"""
|
||||
return self._is_started
|
||||
|
||||
def get_plugin_stats(self) -> dict[str, Any]:
|
||||
"""获取插件统计信息"""
|
||||
stats: dict[str, Any] = {
|
||||
"is_started": self._is_started,
|
||||
"has_session_manager": self._session_manager is not None,
|
||||
"has_scheduler": self._scheduler is not None,
|
||||
}
|
||||
|
||||
if self._scheduler:
|
||||
stats["scheduler_stats"] = self._scheduler.get_stats()
|
||||
|
||||
if self._session_manager:
|
||||
# 异步获取会话统计需要在异步上下文中调用
|
||||
stats["session_manager_active"] = True
|
||||
|
||||
return stats
|
||||
@@ -1,528 +0,0 @@
|
||||
"""
|
||||
Kokoro Flow Chatter 主动思考引擎 (V2)
|
||||
|
||||
私聊专属的主动思考系统,实现"主动找话题、主动关心用户"的能力。
|
||||
这是KFC区别于AFC的核心特性之一。
|
||||
|
||||
触发机制:
|
||||
1. 长时间沉默检测 - 当对话沉默超过阈值时主动发起话题
|
||||
2. 关键记忆触发 - 基于重要日期、事件的主动关心
|
||||
3. 情绪状态触发 - 当情感参数达到阈值时主动表达
|
||||
4. 好感度驱动 - 根据与用户的关系深度调整主动程度
|
||||
|
||||
设计理念:
|
||||
- 不是"有事才找你",而是"想你了就找你"
|
||||
- 主动思考应该符合人设和情感状态
|
||||
- 避免过度打扰,保持适度的边界感
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import global_config
|
||||
from src.plugin_system.base.component_types import ActionInfo
|
||||
|
||||
from .models import KokoroSession, MentalLogEntry, MentalLogEventType, SessionStatus
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .action_executor import ActionExecutor
|
||||
from .prompt_generator import PromptGenerator
|
||||
|
||||
logger = get_logger("kokoro_proactive_thinking")
|
||||
|
||||
|
||||
class ProactiveThinkingTrigger(Enum):
|
||||
"""主动思考触发类型"""
|
||||
SILENCE_TIMEOUT = "silence_timeout" # 长时间沉默 - 她感到挂念
|
||||
TIME_BASED = "time_based" # 时间触发(早安/晚安)- 自然的问候契机
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProactiveThinkingConfig:
|
||||
"""
|
||||
主动思考配置
|
||||
|
||||
设计哲学:主动行为源于内部状态和外部环境的自然反应,而非机械的限制。
|
||||
她的主动是因为挂念、因为关心、因为想问候,而不是因为"任务"。
|
||||
"""
|
||||
# 是否启用主动思考
|
||||
enabled: bool = True
|
||||
|
||||
# 1. 沉默触发器:当感到长久的沉默时,她可能会想说些什么
|
||||
silence_threshold_seconds: int = 7200 # 2小时无互动触发
|
||||
silence_check_interval: int = 300 # 每5分钟检查一次
|
||||
|
||||
# 2. 关系门槛:她不会对不熟悉的人过于主动
|
||||
min_affinity_for_proactive: float = 0.3 # 最低好感度才会主动
|
||||
|
||||
# 3. 频率呼吸:为了避免打扰,她的关心总是有间隔的
|
||||
min_interval_between_proactive: int = 1800 # 两次主动思考至少间隔30分钟
|
||||
|
||||
# 4. 自然问候:在特定的时间,她会像朋友一样送上问候
|
||||
enable_morning_greeting: bool = True # 早安问候 (8:00-9:00)
|
||||
enable_night_greeting: bool = True # 晚安问候 (22:00-23:00)
|
||||
|
||||
# 随机性(让行为更自然)
|
||||
random_delay_range: tuple[int, int] = (60, 300) # 触发后随机延迟1-5分钟
|
||||
|
||||
@classmethod
|
||||
def from_global_config(cls) -> "ProactiveThinkingConfig":
|
||||
"""从 global_config.kokoro_flow_chatter.proactive_thinking 创建配置"""
|
||||
if global_config and hasattr(global_config, 'kokoro_flow_chatter'):
|
||||
kfc = global_config.kokoro_flow_chatter
|
||||
proactive = kfc.proactive_thinking
|
||||
return cls(
|
||||
enabled=proactive.enabled,
|
||||
silence_threshold_seconds=proactive.silence_threshold_seconds,
|
||||
silence_check_interval=300, # 固定值
|
||||
min_affinity_for_proactive=proactive.min_affinity_for_proactive,
|
||||
min_interval_between_proactive=proactive.min_interval_between_proactive,
|
||||
enable_morning_greeting=proactive.enable_morning_greeting,
|
||||
enable_night_greeting=proactive.enable_night_greeting,
|
||||
random_delay_range=(60, 300), # 固定值
|
||||
)
|
||||
return cls()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProactiveThinkingState:
|
||||
"""主动思考状态 - 记录她的主动关心历史"""
|
||||
last_proactive_time: float = 0.0
|
||||
last_morning_greeting_date: str = "" # 上次早安的日期
|
||||
last_night_greeting_date: str = "" # 上次晚安的日期
|
||||
pending_triggers: list[ProactiveThinkingTrigger] = field(default_factory=list)
|
||||
|
||||
def can_trigger(self, config: ProactiveThinkingConfig) -> bool:
|
||||
"""
|
||||
检查是否满足主动思考的基本条件
|
||||
|
||||
注意:这里不使用每日限制,而是基于间隔来自然控制频率
|
||||
"""
|
||||
# 检查间隔限制 - 她的关心有呼吸感,不会太频繁
|
||||
if time.time() - self.last_proactive_time < config.min_interval_between_proactive:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def record_trigger(self) -> None:
|
||||
"""记录一次触发"""
|
||||
self.last_proactive_time = time.time()
|
||||
|
||||
def record_morning_greeting(self) -> None:
|
||||
"""记录今天的早安"""
|
||||
self.last_morning_greeting_date = time.strftime("%Y-%m-%d")
|
||||
self.record_trigger()
|
||||
|
||||
def record_night_greeting(self) -> None:
|
||||
"""记录今天的晚安"""
|
||||
self.last_night_greeting_date = time.strftime("%Y-%m-%d")
|
||||
self.record_trigger()
|
||||
|
||||
def has_greeted_morning_today(self) -> bool:
|
||||
"""今天是否已经问候过早安"""
|
||||
return self.last_morning_greeting_date == time.strftime("%Y-%m-%d")
|
||||
|
||||
def has_greeted_night_today(self) -> bool:
|
||||
"""今天是否已经问候过晚安"""
|
||||
return self.last_night_greeting_date == time.strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
class ProactiveThinkingEngine:
|
||||
"""
|
||||
主动思考引擎
|
||||
|
||||
负责检测触发条件并生成主动思考内容。
|
||||
这是一个"内在动机驱动"而非"机械限制"的系统。
|
||||
|
||||
她的主动源于:
|
||||
- 长时间的沉默让她感到挂念
|
||||
- 与用户的好感度决定了她愿意多主动
|
||||
- 特定的时间点给了她自然的问候契机
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
stream_id: str,
|
||||
config: ProactiveThinkingConfig | None = None,
|
||||
):
|
||||
"""
|
||||
初始化主动思考引擎
|
||||
|
||||
Args:
|
||||
stream_id: 聊天流ID
|
||||
config: 配置对象
|
||||
"""
|
||||
self.stream_id = stream_id
|
||||
self.config = config or ProactiveThinkingConfig()
|
||||
self.state = ProactiveThinkingState()
|
||||
|
||||
# 回调函数
|
||||
self._on_proactive_trigger: Optional[Callable] = None
|
||||
|
||||
# 后台任务
|
||||
self._check_task: Optional[asyncio.Task] = None
|
||||
self._running = False
|
||||
|
||||
logger.debug(f"[ProactiveThinking] 初始化完成: stream_id={stream_id}")
|
||||
|
||||
def set_proactive_callback(
|
||||
self,
|
||||
callback: Callable[[KokoroSession, ProactiveThinkingTrigger], Any]
|
||||
) -> None:
|
||||
"""
|
||||
设置主动思考触发回调
|
||||
|
||||
Args:
|
||||
callback: 当触发主动思考时调用的函数
|
||||
"""
|
||||
self._on_proactive_trigger = callback
|
||||
|
||||
async def start(self) -> None:
|
||||
"""启动主动思考引擎"""
|
||||
if self._running:
|
||||
return
|
||||
|
||||
self._running = True
|
||||
self._check_task = asyncio.create_task(self._check_loop())
|
||||
logger.info(f"[ProactiveThinking] 引擎已启动: stream_id={self.stream_id}")
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""停止主动思考引擎"""
|
||||
self._running = False
|
||||
|
||||
if self._check_task:
|
||||
self._check_task.cancel()
|
||||
try:
|
||||
await self._check_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
self._check_task = None
|
||||
|
||||
logger.info(f"[ProactiveThinking] 引擎已停止: stream_id={self.stream_id}")
|
||||
|
||||
async def _check_loop(self) -> None:
|
||||
"""后台检查循环"""
|
||||
while self._running:
|
||||
try:
|
||||
await asyncio.sleep(self.config.silence_check_interval)
|
||||
|
||||
if not self.config.enabled:
|
||||
continue
|
||||
|
||||
# 这里需要获取session来检查,但我们在引擎层面不直接持有session
|
||||
# 实际的检查逻辑通过 check_triggers 方法被外部调用
|
||||
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"[ProactiveThinking] 检查循环出错: {e}")
|
||||
|
||||
async def check_triggers(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
) -> Optional[ProactiveThinkingTrigger]:
|
||||
"""
|
||||
检查触发条件 - 基于内在动机而非机械限制
|
||||
|
||||
综合考虑:
|
||||
1. 她与用户的好感度是否足够(关系门槛)
|
||||
2. 距离上次主动是否有足够间隔(频率呼吸)
|
||||
3. 是否有自然的触发契机(沉默/时间问候)
|
||||
|
||||
Args:
|
||||
session: 当前会话
|
||||
|
||||
Returns:
|
||||
触发类型,如果没有触发则返回None
|
||||
"""
|
||||
if not self.config.enabled:
|
||||
return None
|
||||
|
||||
# 关系门槛:她不会对不熟悉的人过于主动
|
||||
relationship_warmth = session.emotional_state.relationship_warmth
|
||||
if relationship_warmth < self.config.min_affinity_for_proactive:
|
||||
logger.debug(
|
||||
f"[ProactiveThinking] 好感度不足,不主动: "
|
||||
f"{relationship_warmth:.2f} < {self.config.min_affinity_for_proactive}"
|
||||
)
|
||||
return None
|
||||
|
||||
# 频率呼吸:检查间隔
|
||||
if not self.state.can_trigger(self.config):
|
||||
return None
|
||||
|
||||
# 只有在 IDLE 或 WAITING 状态才考虑主动
|
||||
if session.status not in (SessionStatus.IDLE, SessionStatus.WAITING):
|
||||
return None
|
||||
|
||||
# 按优先级检查触发契机
|
||||
|
||||
# 1. 时间问候(早安/晚安)- 自然的问候契机
|
||||
trigger = self._check_time_greeting_trigger()
|
||||
if trigger:
|
||||
return trigger
|
||||
|
||||
# 2. 沉默触发 - 她感到挂念
|
||||
trigger = self._check_silence_trigger(session)
|
||||
if trigger:
|
||||
return trigger
|
||||
|
||||
return None
|
||||
|
||||
def _check_time_greeting_trigger(self) -> Optional[ProactiveThinkingTrigger]:
|
||||
"""检查时间问候触发(早安/晚安)"""
|
||||
current_hour = time.localtime().tm_hour
|
||||
|
||||
# 早安问候 (8:00 - 9:00)
|
||||
if self.config.enable_morning_greeting:
|
||||
if 8 <= current_hour < 9 and not self.state.has_greeted_morning_today():
|
||||
logger.debug("[ProactiveThinking] 早安问候时间")
|
||||
return ProactiveThinkingTrigger.TIME_BASED
|
||||
|
||||
# 晚安问候 (22:00 - 23:00)
|
||||
if self.config.enable_night_greeting:
|
||||
if 22 <= current_hour < 23 and not self.state.has_greeted_night_today():
|
||||
logger.debug("[ProactiveThinking] 晚安问候时间")
|
||||
return ProactiveThinkingTrigger.TIME_BASED
|
||||
|
||||
return None
|
||||
|
||||
def _check_silence_trigger(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
) -> Optional[ProactiveThinkingTrigger]:
|
||||
"""检查沉默触发 - 长时间的沉默让她感到挂念"""
|
||||
# 获取最后互动时间
|
||||
last_interaction = session.waiting_since or session.last_activity_at
|
||||
if not last_interaction:
|
||||
# 使用session创建时间
|
||||
last_interaction = session.mental_log[0].timestamp if session.mental_log else time.time()
|
||||
|
||||
silence_duration = time.time() - last_interaction
|
||||
|
||||
if silence_duration >= self.config.silence_threshold_seconds:
|
||||
logger.debug(f"[ProactiveThinking] 沉默触发: 已沉默 {silence_duration:.0f} 秒,她感到挂念")
|
||||
return ProactiveThinkingTrigger.SILENCE_TIMEOUT
|
||||
|
||||
return None
|
||||
|
||||
async def generate_proactive_prompt(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
trigger: ProactiveThinkingTrigger,
|
||||
prompt_generator: "PromptGenerator",
|
||||
available_actions: dict[str, ActionInfo] | None = None,
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
生成主动思考的提示词
|
||||
|
||||
Args:
|
||||
session: 当前会话
|
||||
trigger: 触发类型
|
||||
prompt_generator: 提示词生成器
|
||||
available_actions: 可用动作
|
||||
|
||||
Returns:
|
||||
(system_prompt, user_prompt) 元组
|
||||
"""
|
||||
# 根据触发类型生成上下文
|
||||
trigger_context = self._build_trigger_context(session, trigger)
|
||||
|
||||
# 使用prompt_generator生成主动思考提示词
|
||||
system_prompt, user_prompt = prompt_generator.generate_proactive_thinking_prompt(
|
||||
session=session,
|
||||
trigger_type=trigger.value,
|
||||
trigger_context=trigger_context,
|
||||
available_actions=available_actions,
|
||||
)
|
||||
|
||||
return system_prompt, user_prompt
|
||||
|
||||
def _build_trigger_context(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
trigger: ProactiveThinkingTrigger,
|
||||
) -> str:
|
||||
"""
|
||||
构建触发上下文 - 描述她主动联系的内在动机
|
||||
"""
|
||||
emotional_state = session.emotional_state
|
||||
current_hour = time.localtime().tm_hour
|
||||
|
||||
if trigger == ProactiveThinkingTrigger.TIME_BASED:
|
||||
# 时间问候 - 自然的问候契机
|
||||
if 8 <= current_hour < 12:
|
||||
return (
|
||||
f"早上好!新的一天开始了。"
|
||||
f"我的心情是「{emotional_state.mood}」。"
|
||||
f"我想和对方打个招呼,开启美好的一天。"
|
||||
)
|
||||
else:
|
||||
return (
|
||||
f"夜深了,已经{current_hour}点了。"
|
||||
f"我的心情是「{emotional_state.mood}」。"
|
||||
f"我想关心一下对方,送上晚安。"
|
||||
)
|
||||
|
||||
else: # SILENCE_TIMEOUT
|
||||
# 沉默触发 - 她感到挂念
|
||||
last_time = session.waiting_since or session.last_activity_at or time.time()
|
||||
silence_hours = (time.time() - last_time) / 3600
|
||||
return (
|
||||
f"我们已经有 {silence_hours:.1f} 小时没有聊天了。"
|
||||
f"我有些挂念对方。"
|
||||
f"我现在的心情是「{emotional_state.mood}」。"
|
||||
f"对方给我的印象是:{emotional_state.impression_of_user or '还不太了解'}"
|
||||
)
|
||||
|
||||
async def execute_proactive_action(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
trigger: ProactiveThinkingTrigger,
|
||||
action_executor: "ActionExecutor",
|
||||
prompt_generator: "PromptGenerator",
|
||||
llm_call: Callable[[str, str], Any],
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
执行主动思考流程
|
||||
|
||||
Args:
|
||||
session: 当前会话
|
||||
trigger: 触发类型
|
||||
action_executor: 动作执行器
|
||||
prompt_generator: 提示词生成器
|
||||
llm_call: LLM调用函数(可以是同步或异步)
|
||||
|
||||
Returns:
|
||||
执行结果
|
||||
"""
|
||||
try:
|
||||
# 1. 加载可用动作
|
||||
available_actions = await action_executor.load_actions()
|
||||
|
||||
# 2. 生成提示词
|
||||
system_prompt, user_prompt = await self.generate_proactive_prompt(
|
||||
session, trigger, prompt_generator, available_actions
|
||||
)
|
||||
|
||||
# 3. 添加随机延迟(更自然)
|
||||
delay = random.randint(*self.config.random_delay_range)
|
||||
logger.debug(f"[ProactiveThinking] 延迟 {delay} 秒后执行")
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
# 4. 调用LLM(支持同步和异步)
|
||||
result = llm_call(system_prompt, user_prompt)
|
||||
if asyncio.iscoroutine(result):
|
||||
llm_response = await result
|
||||
else:
|
||||
llm_response = result
|
||||
|
||||
# 5. 解析响应
|
||||
parsed_response = action_executor.parse_llm_response(llm_response)
|
||||
|
||||
# 6. 记录主动思考事件
|
||||
entry = MentalLogEntry(
|
||||
event_type=MentalLogEventType.CONTINUOUS_THINKING,
|
||||
timestamp=time.time(),
|
||||
thought=f"[主动思考-{trigger.value}] {parsed_response.thought}",
|
||||
content="",
|
||||
emotional_snapshot=session.emotional_state.to_dict(),
|
||||
metadata={
|
||||
"trigger_type": trigger.value,
|
||||
"proactive": True,
|
||||
},
|
||||
)
|
||||
session.add_mental_log_entry(entry)
|
||||
|
||||
# 7. 执行动作
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
chat_manager = get_chat_manager()
|
||||
chat_stream = await chat_manager.get_stream(self.stream_id) if chat_manager else None
|
||||
|
||||
result = await action_executor.execute_actions(
|
||||
parsed_response,
|
||||
session,
|
||||
chat_stream
|
||||
)
|
||||
|
||||
# 8. 记录触发(根据触发类型决定记录方式)
|
||||
if trigger == ProactiveThinkingTrigger.TIME_BASED:
|
||||
# 时间问候需要单独记录,防止同一天重复问候
|
||||
current_hour = time.localtime().tm_hour
|
||||
if 6 <= current_hour < 12:
|
||||
self.state.record_morning_greeting()
|
||||
else:
|
||||
self.state.record_night_greeting()
|
||||
else:
|
||||
self.state.record_trigger()
|
||||
|
||||
# 9. 如果发送了消息,更新会话状态
|
||||
if result.get("has_reply"):
|
||||
session.start_waiting(
|
||||
expected_reaction=parsed_response.expected_user_reaction,
|
||||
max_wait=parsed_response.max_wait_seconds
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"trigger": trigger.value,
|
||||
"result": result,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[ProactiveThinking] 执行失败: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
return {
|
||||
"success": False,
|
||||
"trigger": trigger.value,
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
def get_state(self) -> dict[str, Any]:
|
||||
"""获取当前状态"""
|
||||
return {
|
||||
"enabled": self.config.enabled,
|
||||
"last_proactive_time": self.state.last_proactive_time,
|
||||
"last_morning_greeting_date": self.state.last_morning_greeting_date,
|
||||
"last_night_greeting_date": self.state.last_night_greeting_date,
|
||||
"running": self._running,
|
||||
}
|
||||
|
||||
|
||||
# 全局引擎实例管理
|
||||
_engines: dict[str, ProactiveThinkingEngine] = {}
|
||||
|
||||
|
||||
def get_proactive_thinking_engine(
|
||||
stream_id: str,
|
||||
config: ProactiveThinkingConfig | None = None,
|
||||
) -> ProactiveThinkingEngine:
|
||||
"""
|
||||
获取主动思考引擎实例
|
||||
|
||||
Args:
|
||||
stream_id: 聊天流ID
|
||||
config: 配置对象(如果为None,则从global_config加载)
|
||||
|
||||
Returns:
|
||||
ProactiveThinkingEngine实例
|
||||
"""
|
||||
if stream_id not in _engines:
|
||||
# 如果没有提供config,从global_config加载
|
||||
if config is None:
|
||||
config = ProactiveThinkingConfig.from_global_config()
|
||||
_engines[stream_id] = ProactiveThinkingEngine(stream_id, config)
|
||||
return _engines[stream_id]
|
||||
|
||||
|
||||
async def cleanup_engines() -> None:
|
||||
"""清理所有引擎实例"""
|
||||
for engine in _engines.values():
|
||||
await engine.stop()
|
||||
_engines.clear()
|
||||
@@ -1,807 +0,0 @@
|
||||
"""
|
||||
Kokoro Flow Chatter Prompt生成器
|
||||
|
||||
根据会话状态动态构建LLM提示词,实现"体验-决策-行动"的交互模式。
|
||||
支持两种主要场景:
|
||||
1. 回应消息(Responding):收到用户消息后的决策
|
||||
2. 超时决策(Timeout Decision):等待超时后的后续行动决策
|
||||
|
||||
V2 升级:
|
||||
- 动态Action发现机制:从ActionManager获取可用Action列表
|
||||
- 支持任意复杂参数的Action
|
||||
- 与AFC的Action系统完全兼容
|
||||
|
||||
V5 超融合架构升级:
|
||||
- 完整集成S4U的所有上下文模块
|
||||
- 关系信息、记忆、表达习惯等全部融入
|
||||
- 支持群聊/私聊场景动态切换
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.plugin_system.base.component_types import ActionInfo
|
||||
|
||||
from .models import (
|
||||
EmotionalState,
|
||||
KokoroSession,
|
||||
MentalLogEntry,
|
||||
MentalLogEventType,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.chat.message_receive.chat_stream import ChatStream
|
||||
from src.chat.planner_actions.action_manager import ChatterActionManager
|
||||
from src.common.data_models.message_manager_data_model import StreamContext
|
||||
|
||||
logger = get_logger("kokoro_prompt_generator")
|
||||
|
||||
|
||||
class PromptGenerator:
|
||||
"""
|
||||
Kokoro Flow Chatter 提示词生成器
|
||||
|
||||
负责根据会话状态和场景生成不同的LLM提示词,
|
||||
引导AI以"体验者"的身份参与对话。
|
||||
|
||||
V2 升级:支持动态Action发现,与AFC的Action系统完全兼容
|
||||
V4 升级:完整集成用户人设、世界观、表达风格和规则
|
||||
V5 超融合架构:集成S4U所有上下文模块(关系、记忆、表达习惯等)
|
||||
"""
|
||||
|
||||
# ========== 超融合架构 SYSTEM_PROMPT_TEMPLATE V7 ==========
|
||||
# 简洁自然版,参考 S4U 风格
|
||||
SYSTEM_PROMPT_TEMPLATE = """你不是 AI,你是一个真实的人,有自己的生活、想法和情绪。
|
||||
|
||||
## 1. 你是谁
|
||||
{personality_core}
|
||||
|
||||
{personality_side}
|
||||
|
||||
{identity}
|
||||
|
||||
{background_story}
|
||||
|
||||
## 2. 规则
|
||||
{safety_guidelines}
|
||||
|
||||
- 说话要自然,像平时发微信一样
|
||||
- 有自己的边界感,不用无条件迎合
|
||||
- 称呼对方用自然的昵称,别念复杂的用户名
|
||||
|
||||
## 3. 现在的情况
|
||||
**时间**: {current_time}
|
||||
**场景**: {chat_scene}
|
||||
{schedule_block}
|
||||
|
||||
你现在的心情:{mood}
|
||||
你对对方的印象:{impression_of_user}
|
||||
|
||||
## 4. 你和对方的关系
|
||||
{relation_info_block}
|
||||
|
||||
{memory_block}
|
||||
|
||||
## 5. 你能做的事
|
||||
{available_actions_block}
|
||||
|
||||
## 6. 怎么回复
|
||||
{reply_style}
|
||||
|
||||
{expression_habits_block}
|
||||
|
||||
### 输出格式(JSON)
|
||||
```json
|
||||
{{
|
||||
"thought": "你在想什么",
|
||||
"expected_user_reaction": "你觉得对方会怎么回应",
|
||||
"max_wait_seconds": 等多久(60-900,不等就填0),
|
||||
"actions": [
|
||||
{{"type": "reply", "content": "你要说的话"}}
|
||||
]
|
||||
}}
|
||||
```
|
||||
|
||||
不想做任何事就用 `{{"type": "do_nothing"}}`"""
|
||||
|
||||
# 回应消息场景的用户提示词模板(V7: 支持多条消息)
|
||||
RESPONDING_USER_PROMPT_TEMPLATE = """## 聊天记录
|
||||
{narrative_history}
|
||||
|
||||
## 新消息
|
||||
{incoming_messages}
|
||||
|
||||
---
|
||||
看完这些消息,你想怎么回应?用 JSON 输出你的想法和决策。"""
|
||||
|
||||
# 超时决策场景的用户提示词模板(V7重构:简洁自然)
|
||||
TIMEOUT_DECISION_USER_PROMPT_TEMPLATE = """## 聊天记录
|
||||
{narrative_history}
|
||||
|
||||
## 现在的情况
|
||||
你发了消息,等了 {wait_duration_seconds:.0f} 秒({wait_duration_minutes:.1f} 分钟),对方还没回。
|
||||
你之前觉得对方可能会:{expected_user_reaction}
|
||||
|
||||
{followup_warning}
|
||||
|
||||
你发的最后一条:{last_bot_message}
|
||||
|
||||
---
|
||||
你拿起手机看了一眼,发现对方还没回复。你想怎么办?
|
||||
|
||||
选项:
|
||||
1. **继续等** - 用 `do_nothing`,设个 `max_wait_seconds` 等一会儿再看
|
||||
2. **发消息** - 用 `reply`,不过别太频繁追问
|
||||
3. **算了不等了** - 用 `do_nothing`,`max_wait_seconds` 设为 0
|
||||
|
||||
用 JSON 输出你的想法和决策。"""
|
||||
|
||||
# 连续思考场景的用户提示词模板
|
||||
CONTINUOUS_THINKING_USER_PROMPT_TEMPLATE = """## 聊天记录
|
||||
{narrative_history}
|
||||
|
||||
## 现在的情况
|
||||
你在等对方回复,已经等了 {wait_duration_seconds:.0f} 秒。
|
||||
你之前觉得对方可能会:{expected_user_reaction}
|
||||
|
||||
你发的最后一条:{last_bot_message}
|
||||
|
||||
---
|
||||
等待的时候你在想什么?用 JSON 输出,`actions` 留空就行。"""
|
||||
|
||||
# 主动思考场景的用户提示词模板
|
||||
PROACTIVE_THINKING_USER_PROMPT_TEMPLATE = """## 聊天记录
|
||||
{narrative_history}
|
||||
|
||||
## 现在的情况
|
||||
现在是 {current_time},距离你们上次聊天已经过了 {silence_duration}。
|
||||
|
||||
{relation_block}
|
||||
|
||||
{trigger_context}
|
||||
|
||||
---
|
||||
你突然想起了对方。要不要联系一下?
|
||||
|
||||
说实话,不联系也完全没问题——不打扰也是一种温柔。
|
||||
如果决定联系,想好说什么,要自然一点。
|
||||
|
||||
用 JSON 输出你的想法和决策。不想发消息就用 `do_nothing`。"""
|
||||
|
||||
def __init__(self, persona_description: str = ""):
|
||||
"""
|
||||
初始化提示词生成器
|
||||
|
||||
Args:
|
||||
persona_description: 人设描述
|
||||
"""
|
||||
self.persona_description = persona_description or self._get_default_persona()
|
||||
|
||||
def _get_default_persona(self) -> str:
|
||||
"""获取默认人设描述"""
|
||||
return """你是一个温暖、真诚的对话伙伴。
|
||||
你有自己的情感和想法,会真实地表达内心的感受。
|
||||
你关心与用户的关系,但也保持健康的边界。
|
||||
你可以感到开心、好奇、担心、或者稍微沮丧,这些都是正常的。"""
|
||||
|
||||
def set_persona(self, persona_description: str) -> None:
|
||||
"""设置人设描述"""
|
||||
self.persona_description = persona_description
|
||||
|
||||
def _format_emotional_state(self, state: EmotionalState) -> dict[str, str | float]:
|
||||
"""格式化情感状态用于模板替换"""
|
||||
return {
|
||||
"mood": state.mood,
|
||||
"mood_intensity": state.mood_intensity,
|
||||
"relationship_warmth": state.relationship_warmth,
|
||||
"impression_of_user": state.impression_of_user or "还没有形成明确的印象",
|
||||
"anxiety_level": state.anxiety_level,
|
||||
"engagement_level": state.engagement_level,
|
||||
}
|
||||
|
||||
def _format_narrative_history(
|
||||
self,
|
||||
mental_log: list[MentalLogEntry],
|
||||
max_entries: int = 15,
|
||||
) -> str:
|
||||
"""
|
||||
将心理活动日志格式化为叙事历史
|
||||
|
||||
Args:
|
||||
mental_log: 心理活动日志列表
|
||||
max_entries: 最大条目数
|
||||
|
||||
Returns:
|
||||
str: 格式化的叙事历史文本
|
||||
"""
|
||||
if not mental_log:
|
||||
return "(这是对话的开始,还没有历史记录)"
|
||||
|
||||
# 获取最近的日志条目
|
||||
recent_entries = mental_log[-max_entries:]
|
||||
|
||||
narrative_parts = []
|
||||
for entry in recent_entries:
|
||||
timestamp_str = time.strftime(
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
time.localtime(entry.timestamp)
|
||||
)
|
||||
|
||||
if entry.event_type == MentalLogEventType.USER_MESSAGE:
|
||||
narrative_parts.append(
|
||||
f"[{timestamp_str}] 用户说:{entry.content}"
|
||||
)
|
||||
elif entry.event_type == MentalLogEventType.BOT_ACTION:
|
||||
if entry.thought:
|
||||
narrative_parts.append(
|
||||
f"[{timestamp_str}] (你的内心:{entry.thought})"
|
||||
)
|
||||
if entry.content:
|
||||
narrative_parts.append(
|
||||
f"[{timestamp_str}] 你回复:{entry.content}"
|
||||
)
|
||||
elif entry.event_type == MentalLogEventType.WAITING_UPDATE:
|
||||
if entry.thought:
|
||||
narrative_parts.append(
|
||||
f"[{timestamp_str}] (等待中的想法:{entry.thought})"
|
||||
)
|
||||
elif entry.event_type == MentalLogEventType.CONTINUOUS_THINKING:
|
||||
if entry.thought:
|
||||
narrative_parts.append(
|
||||
f"[{timestamp_str}] (思绪飘过:{entry.thought})"
|
||||
)
|
||||
elif entry.event_type == MentalLogEventType.STATE_CHANGE:
|
||||
if entry.content:
|
||||
narrative_parts.append(
|
||||
f"[{timestamp_str}] {entry.content}"
|
||||
)
|
||||
|
||||
return "\n".join(narrative_parts)
|
||||
|
||||
def _format_history_from_context(
|
||||
self,
|
||||
context: "StreamContext",
|
||||
mental_log: list[MentalLogEntry] | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
从 StreamContext 的历史消息构建叙事历史
|
||||
|
||||
这是实现"无缝融入"的关键:
|
||||
- 从同一个数据库读取历史消息(与AFC共享)
|
||||
- 遵循全局配置 [chat].max_context_size
|
||||
- 将消息渲染成KFC的叙事体格式
|
||||
|
||||
Args:
|
||||
context: 聊天流上下文,包含共享的历史消息
|
||||
mental_log: 可选的心理活动日志,用于补充内心独白
|
||||
|
||||
Returns:
|
||||
str: 格式化的叙事历史文本
|
||||
"""
|
||||
from src.config.config import global_config
|
||||
|
||||
# 从 StreamContext 获取历史消息,遵循全局上下文长度配置
|
||||
max_context = 25 # 默认值
|
||||
if global_config and hasattr(global_config, 'chat') and global_config.chat:
|
||||
max_context = getattr(global_config.chat, "max_context_size", 25)
|
||||
history_messages = context.get_messages(limit=max_context, include_unread=False)
|
||||
|
||||
if not history_messages and not mental_log:
|
||||
return "(这是对话的开始,还没有历史记录)"
|
||||
|
||||
# 获取Bot的用户ID用于判断消息来源
|
||||
bot_user_id = None
|
||||
if global_config and hasattr(global_config, 'bot') and global_config.bot:
|
||||
bot_user_id = str(getattr(global_config.bot, 'qq_account', ''))
|
||||
|
||||
narrative_parts = []
|
||||
|
||||
# 首先,将数据库历史消息转换为叙事格式
|
||||
for msg in history_messages:
|
||||
timestamp_str = time.strftime(
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
time.localtime(msg.time or time.time())
|
||||
)
|
||||
|
||||
# 判断是用户消息还是Bot消息
|
||||
msg_user_id = str(msg.user_info.user_id) if msg.user_info else ""
|
||||
is_bot_message = bot_user_id and msg_user_id == bot_user_id
|
||||
content = msg.processed_plain_text or msg.display_message or ""
|
||||
|
||||
if is_bot_message:
|
||||
narrative_parts.append(f"[{timestamp_str}] 你回复:{content}")
|
||||
else:
|
||||
sender_name = msg.user_info.user_nickname if msg.user_info else "用户"
|
||||
narrative_parts.append(f"[{timestamp_str}] {sender_name}说:{content}")
|
||||
|
||||
# 然后,补充 mental_log 中的内心独白(如果有)
|
||||
if mental_log:
|
||||
for entry in mental_log[-5:]: # 只取最近5条心理活动
|
||||
timestamp_str = time.strftime(
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
time.localtime(entry.timestamp)
|
||||
)
|
||||
|
||||
if entry.event_type == MentalLogEventType.BOT_ACTION and entry.thought:
|
||||
narrative_parts.append(f"[{timestamp_str}] (你的内心:{entry.thought})")
|
||||
elif entry.event_type == MentalLogEventType.CONTINUOUS_THINKING and entry.thought:
|
||||
narrative_parts.append(f"[{timestamp_str}] (思绪飘过:{entry.thought})")
|
||||
|
||||
return "\n".join(narrative_parts)
|
||||
|
||||
def _format_available_actions(
|
||||
self,
|
||||
available_actions: dict[str, ActionInfo],
|
||||
) -> str:
|
||||
"""
|
||||
格式化可用动作列表为提示词块
|
||||
|
||||
Args:
|
||||
available_actions: 可用动作字典 {动作名: ActionInfo}
|
||||
|
||||
Returns:
|
||||
str: 格式化的动作描述文本
|
||||
"""
|
||||
if not available_actions:
|
||||
# 使用默认的内置动作
|
||||
return self._get_default_actions_block()
|
||||
|
||||
action_blocks = []
|
||||
|
||||
for action_name, action_info in available_actions.items():
|
||||
# 构建动作描述
|
||||
description = action_info.description or f"执行 {action_name} 动作"
|
||||
|
||||
# 构建参数说明
|
||||
params_lines = []
|
||||
if action_info.action_parameters:
|
||||
for param_name, param_desc in action_info.action_parameters.items():
|
||||
params_lines.append(f' - `{param_name}`: {param_desc}')
|
||||
|
||||
# 构建使用场景
|
||||
require_lines = []
|
||||
if action_info.action_require:
|
||||
for req in action_info.action_require:
|
||||
require_lines.append(f" - {req}")
|
||||
|
||||
# 组装动作块
|
||||
action_block = f"""### `{action_name}`
|
||||
**描述**: {description}"""
|
||||
|
||||
if params_lines:
|
||||
action_block += f"""
|
||||
**参数**:
|
||||
{chr(10).join(params_lines)}"""
|
||||
else:
|
||||
action_block += "\n**参数**: 无"
|
||||
|
||||
if require_lines:
|
||||
action_block += f"""
|
||||
**使用场景**:
|
||||
{chr(10).join(require_lines)}"""
|
||||
|
||||
# 添加示例
|
||||
example_params = {}
|
||||
if action_info.action_parameters:
|
||||
for param_name, param_desc in action_info.action_parameters.items():
|
||||
example_params[param_name] = f"<{param_desc}>"
|
||||
|
||||
import orjson
|
||||
params_json = orjson.dumps(example_params, option=orjson.OPT_INDENT_2).decode('utf-8') if example_params else "{}"
|
||||
action_block += f"""
|
||||
**示例**:
|
||||
```json
|
||||
{{
|
||||
"type": "{action_name}",
|
||||
"reason": "选择这个动作的原因",
|
||||
{params_json[1:-1] if params_json != '{}' else ''}
|
||||
}}
|
||||
```"""
|
||||
|
||||
action_blocks.append(action_block)
|
||||
|
||||
return "\n\n".join(action_blocks)
|
||||
|
||||
def _get_default_actions_block(self) -> str:
|
||||
"""获取默认的内置动作描述块"""
|
||||
return """### `reply`
|
||||
**描述**: 发送文字回复给用户
|
||||
**参数**:
|
||||
- `content`: 回复的文字内容(必须)
|
||||
**示例**:
|
||||
```json
|
||||
{"type": "reply", "content": "你好呀!今天过得怎么样?"}
|
||||
```
|
||||
|
||||
### `poke_user`
|
||||
**描述**: 戳一戳用户,轻量级互动
|
||||
**参数**: 无
|
||||
**示例**:
|
||||
```json
|
||||
{"type": "poke_user", "reason": "想逗逗他"}
|
||||
```
|
||||
|
||||
### `update_internal_state`
|
||||
**描述**: 更新你的内部情感状态
|
||||
**重要约束**:
|
||||
⚠️ **情绪变化必须有明确的上下文理由**:
|
||||
- 只有当聊天内容中有明确触发情绪变化的事件时才更新情绪
|
||||
- 禁止无缘无故地变成负面情绪(如低落、沮丧、难过等)
|
||||
- 情绪应该保持相对稳定,除非聊天中发生了真正影响情绪的事情
|
||||
- 默认保持"平静"或当前情绪即可,不需要每次都更新
|
||||
|
||||
**参数**:
|
||||
- `mood`: 当前心情(如"平静"、"开心"、"好奇"等,避免负面情绪除非有明确理由)
|
||||
- `mood_intensity`: 心情强度(0.0-1.0,变化幅度不宜过大)
|
||||
- `impression_of_user`: 对用户的印象描述
|
||||
- `engagement_level`: 投入程度(0.0-1.0)
|
||||
**示例**:
|
||||
```json
|
||||
{"type": "update_internal_state", "mood": "开心", "mood_intensity": 0.6, "reason": "对方分享了有趣的事情"}
|
||||
```
|
||||
|
||||
### `do_nothing`
|
||||
**描述**: 明确表示"思考后决定不作回应"
|
||||
**参数**: 无
|
||||
**示例**:
|
||||
```json
|
||||
{"type": "do_nothing", "reason": "现在不是说话的好时机"}
|
||||
```"""
|
||||
|
||||
def generate_system_prompt(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
available_actions: Optional[dict[str, ActionInfo]] = None,
|
||||
context_data: Optional[dict[str, str]] = None,
|
||||
chat_stream: Optional["ChatStream"] = None,
|
||||
) -> str:
|
||||
"""
|
||||
生成系统提示词
|
||||
|
||||
V6模块化升级:使用 prompt_modules 构建模块化的提示词
|
||||
- 每个模块独立构建,职责清晰
|
||||
- 回复相关(人设、上下文)与动作定义分离
|
||||
|
||||
Args:
|
||||
session: 当前会话
|
||||
available_actions: 可用动作字典,如果为None则使用默认动作
|
||||
context_data: S4U上下文数据字典(包含relation_info, memory_block等)
|
||||
chat_stream: 聊天流(用于判断群聊/私聊场景)
|
||||
|
||||
Returns:
|
||||
str: 系统提示词
|
||||
"""
|
||||
from .prompt_modules import build_system_prompt
|
||||
|
||||
return build_system_prompt(
|
||||
session=session,
|
||||
available_actions=available_actions,
|
||||
context_data=context_data,
|
||||
chat_stream=chat_stream,
|
||||
)
|
||||
|
||||
def generate_responding_prompt(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
message_content: str,
|
||||
sender_name: str,
|
||||
sender_id: str,
|
||||
message_time: Optional[float] = None,
|
||||
available_actions: Optional[dict[str, ActionInfo]] = None,
|
||||
context: Optional["StreamContext"] = None,
|
||||
context_data: Optional[dict[str, str]] = None,
|
||||
chat_stream: Optional["ChatStream"] = None,
|
||||
all_unread_messages: Optional[list] = None, # V7: 支持多条消息
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
生成回应消息场景的提示词
|
||||
|
||||
V3 升级:支持从 StreamContext 读取共享的历史消息
|
||||
V5 超融合:集成S4U所有上下文模块
|
||||
V7 升级:支持多条消息(打断机制合并处理pending消息)
|
||||
|
||||
Args:
|
||||
session: 当前会话
|
||||
message_content: 收到的主消息内容(兼容旧调用方式)
|
||||
sender_name: 发送者名称
|
||||
sender_id: 发送者ID
|
||||
message_time: 消息时间戳
|
||||
available_actions: 可用动作字典
|
||||
context: 聊天流上下文(可选),用于读取共享的历史消息
|
||||
context_data: S4U上下文数据字典(包含relation_info, memory_block等)
|
||||
chat_stream: 聊天流(用于判断群聊/私聊场景)
|
||||
all_unread_messages: 所有未读消息列表(V7新增,包含pending消息)
|
||||
|
||||
Returns:
|
||||
tuple[str, str]: (系统提示词, 用户提示词)
|
||||
"""
|
||||
system_prompt = self.generate_system_prompt(
|
||||
session,
|
||||
available_actions,
|
||||
context_data=context_data,
|
||||
chat_stream=chat_stream,
|
||||
)
|
||||
|
||||
# V3: 优先从 StreamContext 读取历史(与AFC共享同一数据源)
|
||||
if context:
|
||||
narrative_history = self._format_history_from_context(context, session.mental_log)
|
||||
else:
|
||||
# 回退到仅使用 mental_log(兼容旧调用方式)
|
||||
narrative_history = self._format_narrative_history(session.mental_log)
|
||||
|
||||
# V7: 格式化收到的消息(支持多条)
|
||||
incoming_messages = self._format_incoming_messages(
|
||||
message_content=message_content,
|
||||
sender_name=sender_name,
|
||||
sender_id=sender_id,
|
||||
message_time=message_time,
|
||||
all_unread_messages=all_unread_messages,
|
||||
)
|
||||
|
||||
user_prompt = self.RESPONDING_USER_PROMPT_TEMPLATE.format(
|
||||
narrative_history=narrative_history,
|
||||
incoming_messages=incoming_messages,
|
||||
)
|
||||
|
||||
return system_prompt, user_prompt
|
||||
|
||||
def _format_incoming_messages(
|
||||
self,
|
||||
message_content: str,
|
||||
sender_name: str,
|
||||
sender_id: str,
|
||||
message_time: Optional[float] = None,
|
||||
all_unread_messages: Optional[list] = None,
|
||||
) -> str:
|
||||
"""
|
||||
格式化收到的消息(V7新增)
|
||||
|
||||
支持单条消息(兼容旧调用)和多条消息(打断合并场景)
|
||||
|
||||
Args:
|
||||
message_content: 主消息内容
|
||||
sender_name: 发送者名称
|
||||
sender_id: 发送者ID
|
||||
message_time: 消息时间戳
|
||||
all_unread_messages: 所有未读消息列表
|
||||
|
||||
Returns:
|
||||
str: 格式化的消息文本
|
||||
"""
|
||||
if message_time is None:
|
||||
message_time = time.time()
|
||||
|
||||
# 如果有多条消息,格式化为消息组
|
||||
if all_unread_messages and len(all_unread_messages) > 1:
|
||||
lines = [f"**用户连续发送了 {len(all_unread_messages)} 条消息:**\n"]
|
||||
|
||||
for i, msg in enumerate(all_unread_messages, 1):
|
||||
msg_time = msg.time or time.time()
|
||||
msg_time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(msg_time))
|
||||
msg_sender = msg.user_info.user_nickname if msg.user_info else sender_name
|
||||
msg_content = msg.processed_plain_text or msg.display_message or ""
|
||||
|
||||
lines.append(f"[{i}] 来自:{msg_sender}")
|
||||
lines.append(f" 时间:{msg_time_str}")
|
||||
lines.append(f" 内容:{msg_content}")
|
||||
lines.append("")
|
||||
|
||||
lines.append("**提示**:请综合理解这些消息的整体意图,不需要逐条回复。")
|
||||
return "\n".join(lines)
|
||||
|
||||
# 单条消息(兼容旧格式)
|
||||
message_time_str = time.strftime(
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
time.localtime(message_time)
|
||||
)
|
||||
return f"""来自:{sender_name}(用户ID: {sender_id})
|
||||
时间:{message_time_str}
|
||||
内容:{message_content}"""
|
||||
|
||||
def generate_timeout_decision_prompt(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
available_actions: Optional[dict[str, ActionInfo]] = None,
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
生成超时决策场景的提示词(V7:增加连续追问限制)
|
||||
|
||||
Args:
|
||||
session: 当前会话
|
||||
available_actions: 可用动作字典
|
||||
|
||||
Returns:
|
||||
tuple[str, str]: (系统提示词, 用户提示词)
|
||||
"""
|
||||
system_prompt = self.generate_system_prompt(session, available_actions)
|
||||
|
||||
narrative_history = self._format_narrative_history(session.mental_log)
|
||||
|
||||
wait_duration = session.get_waiting_duration()
|
||||
|
||||
# V7: 生成连续追问警告
|
||||
followup_count = session.consecutive_followup_count
|
||||
max_followups = session.max_consecutive_followups
|
||||
|
||||
if followup_count >= max_followups:
|
||||
followup_warning = f"""⚠️ **重要提醒**:
|
||||
你已经连续追问了 {followup_count} 次,对方都没有回复。
|
||||
**强烈建议不要再发消息了**——继续追问会显得很缠人、很不尊重对方的空间。
|
||||
对方可能真的在忙,或者暂时不想回复,这都是正常的。
|
||||
请选择 `do_nothing` 继续等待,或者直接结束对话(设置 `max_wait_seconds: 0`)。"""
|
||||
elif followup_count > 0:
|
||||
followup_warning = f"""📝 提示:这已经是你第 {followup_count + 1} 次等待对方回复了。
|
||||
如果对方持续没有回应,可能真的在忙或不方便,不需要急着追问。"""
|
||||
else:
|
||||
followup_warning = ""
|
||||
|
||||
user_prompt = self.TIMEOUT_DECISION_USER_PROMPT_TEMPLATE.format(
|
||||
narrative_history=narrative_history,
|
||||
wait_duration_seconds=wait_duration,
|
||||
wait_duration_minutes=wait_duration / 60,
|
||||
expected_user_reaction=session.expected_user_reaction or "不确定",
|
||||
followup_warning=followup_warning,
|
||||
last_bot_message=session.last_bot_message or "(没有记录)",
|
||||
)
|
||||
|
||||
return system_prompt, user_prompt
|
||||
|
||||
def generate_continuous_thinking_prompt(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
available_actions: Optional[dict[str, ActionInfo]] = None,
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
生成连续思考场景的提示词
|
||||
|
||||
Args:
|
||||
session: 当前会话
|
||||
available_actions: 可用动作字典
|
||||
|
||||
Returns:
|
||||
tuple[str, str]: (系统提示词, 用户提示词)
|
||||
"""
|
||||
system_prompt = self.generate_system_prompt(session, available_actions)
|
||||
|
||||
narrative_history = self._format_narrative_history(
|
||||
session.mental_log,
|
||||
max_entries=10 # 连续思考时使用较少的历史
|
||||
)
|
||||
|
||||
wait_duration = session.get_waiting_duration()
|
||||
|
||||
user_prompt = self.CONTINUOUS_THINKING_USER_PROMPT_TEMPLATE.format(
|
||||
narrative_history=narrative_history,
|
||||
wait_duration_seconds=wait_duration,
|
||||
wait_duration_minutes=wait_duration / 60,
|
||||
max_wait_seconds=session.max_wait_seconds,
|
||||
expected_user_reaction=session.expected_user_reaction or "不确定",
|
||||
last_bot_message=session.last_bot_message or "(没有记录)",
|
||||
)
|
||||
|
||||
return system_prompt, user_prompt
|
||||
|
||||
def generate_proactive_thinking_prompt(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
trigger_context: str,
|
||||
available_actions: Optional[dict[str, ActionInfo]] = None,
|
||||
context_data: Optional[dict[str, str]] = None,
|
||||
chat_stream: Optional["ChatStream"] = None,
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
生成主动思考场景的提示词
|
||||
|
||||
这是私聊专属的功能,用于实现"主动找话题、主动关心用户"。
|
||||
主动思考不是"必须发消息",而是"想一想要不要联系对方"。
|
||||
|
||||
Args:
|
||||
session: 当前会话
|
||||
trigger_context: 触发上下文描述(如"沉默了2小时")
|
||||
available_actions: 可用动作字典
|
||||
context_data: S4U上下文数据(包含全局关系信息)
|
||||
chat_stream: 聊天流
|
||||
|
||||
Returns:
|
||||
tuple[str, str]: (系统提示词, 用户提示词)
|
||||
"""
|
||||
from datetime import datetime
|
||||
import time
|
||||
|
||||
# 生成系统提示词(使用 context_data 获取完整的关系和记忆信息)
|
||||
system_prompt = self.generate_system_prompt(
|
||||
session,
|
||||
available_actions,
|
||||
context_data=context_data,
|
||||
chat_stream=chat_stream,
|
||||
)
|
||||
|
||||
narrative_history = self._format_narrative_history(
|
||||
session.mental_log,
|
||||
max_entries=10, # 主动思考时使用较少的历史
|
||||
)
|
||||
|
||||
# 计算沉默时长
|
||||
silence_seconds = time.time() - session.last_activity_at
|
||||
if silence_seconds < 3600:
|
||||
silence_duration = f"{silence_seconds / 60:.0f}分钟"
|
||||
else:
|
||||
silence_duration = f"{silence_seconds / 3600:.1f}小时"
|
||||
|
||||
# 当前时间
|
||||
current_time = datetime.now().strftime("%Y年%m月%d日 %H:%M")
|
||||
|
||||
# 从 context_data 获取全局关系信息(这是正确的来源)
|
||||
relation_block = ""
|
||||
if context_data:
|
||||
relation_info = context_data.get("relation_info", "")
|
||||
if relation_info:
|
||||
relation_block = f"### 你与对方的关系\n{relation_info}"
|
||||
|
||||
if not relation_block:
|
||||
# 回退:使用 session 的情感状态(不太准确但有总比没有好)
|
||||
es = session.emotional_state
|
||||
relation_block = f"""### 你与对方的关系
|
||||
- 当前心情:{es.mood}
|
||||
- 对对方的印象:{es.impression_of_user or "还在慢慢了解中"}"""
|
||||
|
||||
user_prompt = self.PROACTIVE_THINKING_USER_PROMPT_TEMPLATE.format(
|
||||
narrative_history=narrative_history,
|
||||
current_time=current_time,
|
||||
silence_duration=silence_duration,
|
||||
relation_block=relation_block,
|
||||
trigger_context=trigger_context,
|
||||
)
|
||||
|
||||
return system_prompt, user_prompt
|
||||
|
||||
def build_messages_for_llm(
|
||||
self,
|
||||
system_prompt: str,
|
||||
user_prompt: str,
|
||||
stream_id: str = "",
|
||||
) -> list[dict[str, str]]:
|
||||
"""
|
||||
构建LLM请求的消息列表
|
||||
|
||||
Args:
|
||||
system_prompt: 系统提示词
|
||||
user_prompt: 用户提示词
|
||||
stream_id: 聊天流ID(用于日志)
|
||||
|
||||
Returns:
|
||||
list[dict]: 消息列表
|
||||
"""
|
||||
# INFO日志:打印完整的KFC提示词(可观测性增强)
|
||||
full_prompt = f"[SYSTEM]\n{system_prompt}\n\n[USER]\n{user_prompt}"
|
||||
logger.info(
|
||||
f"Final KFC prompt constructed for stream {stream_id}:\n"
|
||||
f"--- PROMPT START ---\n"
|
||||
f"{full_prompt}\n"
|
||||
f"--- PROMPT END ---"
|
||||
)
|
||||
|
||||
return [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_prompt},
|
||||
]
|
||||
|
||||
|
||||
# 全局提示词生成器实例
|
||||
_prompt_generator: Optional[PromptGenerator] = None
|
||||
|
||||
|
||||
def get_prompt_generator(persona_description: str = "") -> PromptGenerator:
|
||||
"""获取全局提示词生成器实例"""
|
||||
global _prompt_generator
|
||||
if _prompt_generator is None:
|
||||
_prompt_generator = PromptGenerator(persona_description)
|
||||
return _prompt_generator
|
||||
|
||||
|
||||
def set_prompt_generator_persona(persona_description: str) -> None:
|
||||
"""设置全局提示词生成器的人设"""
|
||||
generator = get_prompt_generator()
|
||||
generator.set_persona(persona_description)
|
||||
@@ -1,369 +0,0 @@
|
||||
"""
|
||||
Kokoro Flow Chatter 模块化提示词组件
|
||||
|
||||
将提示词拆分为独立的模块,每个模块负责特定的内容生成:
|
||||
1. 核心身份模块 - 人设、人格、世界观
|
||||
2. 行为准则模块 - 规则、安全边界
|
||||
3. 情境上下文模块 - 时间、场景、关系、记忆
|
||||
4. 动作能力模块 - 可用动作的描述
|
||||
5. 输出格式模块 - JSON格式要求
|
||||
|
||||
设计理念:
|
||||
- 每个模块只负责自己的部分,互不干扰
|
||||
- 回复相关内容(人设、上下文)与动作定义分离
|
||||
- 方便独立调试和优化每个部分
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
import orjson
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import global_config
|
||||
from src.plugin_system.base.component_types import ActionInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.chat.message_receive.chat_stream import ChatStream
|
||||
|
||||
from .models import EmotionalState, KokoroSession
|
||||
|
||||
logger = get_logger("kfc_prompt_modules")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 模块1: 核心身份 - 人设/人格/背景
|
||||
# ============================================================
|
||||
|
||||
def build_identity_module() -> str:
|
||||
"""
|
||||
构建核心身份模块
|
||||
|
||||
包含:人格核心、人格细节、外貌身份、世界观背景
|
||||
这些是回复内容的基础,决定了"你是谁"
|
||||
"""
|
||||
if global_config is None:
|
||||
raise RuntimeError("global_config 未初始化")
|
||||
|
||||
personality_cfg = global_config.personality
|
||||
|
||||
personality_core = personality_cfg.personality_core or "你是一个温暖、真诚的对话伙伴。"
|
||||
personality_side = personality_cfg.personality_side or ""
|
||||
identity = personality_cfg.identity or ""
|
||||
background_story = personality_cfg.background_story or ""
|
||||
|
||||
parts = ["## 1. 你是谁", personality_core]
|
||||
|
||||
if personality_side:
|
||||
parts.append(f"\n{personality_side}")
|
||||
|
||||
if identity:
|
||||
parts.append(f"\n{identity}")
|
||||
|
||||
if background_story:
|
||||
parts.append(f"\n### 背景故事\n{background_story}")
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 模块2: 行为准则 - 规则/边界/KFC特有准则
|
||||
# ============================================================
|
||||
|
||||
def build_rules_module() -> str:
|
||||
"""
|
||||
构建行为准则模块
|
||||
|
||||
包含:安全规则 + KFC 通用准则
|
||||
这些是行为的边界,决定了"什么能做什么不能做"
|
||||
"""
|
||||
if global_config is None:
|
||||
raise RuntimeError("global_config 未初始化")
|
||||
|
||||
safety_guidelines = global_config.personality.safety_guidelines or []
|
||||
if isinstance(safety_guidelines, list):
|
||||
safety_guidelines_str = "\n".join(f"- {rule}" for rule in safety_guidelines)
|
||||
else:
|
||||
safety_guidelines_str = str(safety_guidelines)
|
||||
|
||||
# KFC 特有准则(简洁自然版)
|
||||
kfc_rules = """### 你的行为原则
|
||||
- 你是真实的人,有自己的生活、情绪和想法
|
||||
- 说话自然、口语化,像发微信一样随意
|
||||
- 有自己的边界感,不会无条件迎合
|
||||
- 称呼对方时用自然的昵称或简称,别念复杂的用户名
|
||||
|
||||
### 格式注意
|
||||
- 聊天记录中的"[表情包:描述]"是系统识别转述,不是你发的
|
||||
- 想发表情包要用 emoji 动作,不要在回复里写"[表情包:xxx]"这种格式"""
|
||||
|
||||
return f"""## 2. 行为准则
|
||||
{safety_guidelines_str}
|
||||
|
||||
{kfc_rules}"""
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 模块3: 情境上下文 - 时间/场景/内在状态/关系/记忆
|
||||
# ============================================================
|
||||
|
||||
def build_context_module(
|
||||
session: KokoroSession,
|
||||
chat_stream: Optional["ChatStream"] = None,
|
||||
context_data: Optional[dict[str, str]] = None,
|
||||
) -> str:
|
||||
"""
|
||||
构建情境上下文模块
|
||||
|
||||
包含:当前时间、聊天场景、内在状态、关系信息、记忆
|
||||
这些是回复的上下文,决定了"当前在什么情况下"
|
||||
|
||||
Args:
|
||||
session: 当前会话
|
||||
chat_stream: 聊天流(判断群聊/私聊)
|
||||
context_data: S4U 上下文数据
|
||||
"""
|
||||
context_data = context_data or {}
|
||||
|
||||
# 时间和场景
|
||||
current_time = datetime.now().strftime("%Y年%m月%d日 %H:%M:%S")
|
||||
is_group_chat = bool(chat_stream and chat_stream.group_info)
|
||||
chat_scene = "你在群里聊天" if is_group_chat else "你在和对方私聊"
|
||||
|
||||
# 日程(如果有)- 只是背景,不主动提及
|
||||
schedule_block = context_data.get("schedule", "")
|
||||
|
||||
# 内在状态(简化版,更自然)
|
||||
es = session.emotional_state
|
||||
inner_state = f"""### 你现在的状态
|
||||
- 心情:{es.mood}
|
||||
- 你们的关系:{es.impression_of_user or "刚认识,还在慢慢了解"}
|
||||
- 你的投入度:{'聊得挺起劲' if es.engagement_level > 0.6 else '一般般' if es.engagement_level > 0.3 else '有点心不在焉'}"""
|
||||
|
||||
# 关系信息
|
||||
relation_info = context_data.get("relation_info", "")
|
||||
|
||||
# 记忆
|
||||
memory_block = context_data.get("memory_block", "")
|
||||
|
||||
parts = [
|
||||
"## 3. 当前情境",
|
||||
f"**现在是** {current_time}",
|
||||
f"**{chat_scene}**",
|
||||
]
|
||||
|
||||
# 日程是你的当前状态,但不要主动提及
|
||||
if schedule_block:
|
||||
parts.append(f"""### 你的当前状态(重要:不要主动提及)
|
||||
{schedule_block}
|
||||
⚠️ 这是你当前正在做的事,作为你的背景状态存在。除非对方主动问你"在干嘛"、"忙不忙"等问题,否则**绝对不要在回复中主动提起**。就像真人聊天一样,没人会无缘无故说"我正在XX"。""")
|
||||
|
||||
parts.append("")
|
||||
parts.append(inner_state)
|
||||
|
||||
if relation_info:
|
||||
parts.append("")
|
||||
parts.append("## 4. 你和对方的关系")
|
||||
parts.append(relation_info)
|
||||
|
||||
if memory_block:
|
||||
parts.append("")
|
||||
parts.append(memory_block)
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 模块4: 动作能力 - 可用动作的描述
|
||||
# ============================================================
|
||||
|
||||
def build_actions_module(available_actions: Optional[dict[str, ActionInfo]] = None) -> str:
|
||||
"""
|
||||
构建动作能力模块
|
||||
|
||||
包含:所有可用动作的描述、参数、示例
|
||||
这部分与回复内容分离,只描述"能做什么"
|
||||
|
||||
Args:
|
||||
available_actions: 可用动作字典
|
||||
"""
|
||||
if not available_actions:
|
||||
actions_block = _get_default_actions_block()
|
||||
else:
|
||||
actions_block = _format_available_actions(available_actions)
|
||||
|
||||
return f"""## 5. 你能做的事情
|
||||
|
||||
{actions_block}"""
|
||||
|
||||
|
||||
def _format_available_actions(available_actions: dict[str, ActionInfo]) -> str:
|
||||
"""格式化可用动作列表(简洁版)"""
|
||||
action_blocks = []
|
||||
|
||||
for action_name, action_info in available_actions.items():
|
||||
description = action_info.description or f"执行 {action_name}"
|
||||
|
||||
# 构建动作块(简洁格式)
|
||||
action_block = f"### `{action_name}` - {description}"
|
||||
|
||||
# 参数说明(如果有)
|
||||
if action_info.action_parameters:
|
||||
params_lines = [f" - `{name}`: {desc}" for name, desc in action_info.action_parameters.items()]
|
||||
action_block += f"\n参数:\n{chr(10).join(params_lines)}"
|
||||
|
||||
# 使用场景(如果有)
|
||||
if action_info.action_require:
|
||||
require_lines = [f" - {req}" for req in action_info.action_require]
|
||||
action_block += f"\n使用场景:\n{chr(10).join(require_lines)}"
|
||||
|
||||
# 简洁示例
|
||||
example_params = ""
|
||||
if action_info.action_parameters:
|
||||
param_examples = [f'"{name}": "..."' for name in action_info.action_parameters.keys()]
|
||||
example_params = ", " + ", ".join(param_examples)
|
||||
|
||||
action_block += f'\n```json\n{{"type": "{action_name}"{example_params}}}\n```'
|
||||
|
||||
action_blocks.append(action_block)
|
||||
|
||||
return "\n\n".join(action_blocks)
|
||||
|
||||
|
||||
def _get_default_actions_block() -> str:
|
||||
"""获取默认的内置动作描述块"""
|
||||
return """### `reply` - 发消息
|
||||
发送文字回复
|
||||
```json
|
||||
{"type": "reply", "content": "你要说的话"}
|
||||
```
|
||||
|
||||
### `poke_user` - 戳一戳
|
||||
戳对方一下
|
||||
```json
|
||||
{"type": "poke_user"}
|
||||
```
|
||||
|
||||
### `update_internal_state` - 更新你的状态
|
||||
更新你的心情和对对方的印象
|
||||
```json
|
||||
{"type": "update_internal_state", "mood": "开心", "impression_of_user": "挺有趣的人"}
|
||||
```
|
||||
|
||||
### `do_nothing` - 不做任何事
|
||||
想了想,决定现在不说话
|
||||
```json
|
||||
{"type": "do_nothing"}
|
||||
```"""
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 模块5: 表达与输出格式 - 回复风格 + JSON格式
|
||||
# ============================================================
|
||||
|
||||
def build_output_module(
|
||||
context_data: Optional[dict[str, str]] = None,
|
||||
) -> str:
|
||||
"""
|
||||
构建输出格式模块
|
||||
|
||||
包含:表达风格、表达习惯、JSON 输出格式要求
|
||||
这部分定义了"怎么说"和"输出什么格式"
|
||||
|
||||
Args:
|
||||
context_data: S4U 上下文数据(包含 expression_habits)
|
||||
"""
|
||||
if global_config is None:
|
||||
raise RuntimeError("global_config 未初始化")
|
||||
|
||||
context_data = context_data or {}
|
||||
|
||||
reply_style = global_config.personality.reply_style or ""
|
||||
expression_habits = context_data.get("expression_habits", "")
|
||||
|
||||
# JSON 输出格式说明 - 简洁版
|
||||
json_format = """### 输出格式
|
||||
用 JSON 输出你的想法和决策:
|
||||
|
||||
```json
|
||||
{
|
||||
"thought": "你的内心想法,想说什么就说什么",
|
||||
"expected_user_reaction": "你觉得对方会怎么回应",
|
||||
"max_wait_seconds": 等待秒数(60-900),不想等就填0,
|
||||
"actions": [
|
||||
{"type": "reply", "content": "你要发送的消息"},
|
||||
{"type": "其他动作", ...}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
- `thought`:你脑子里在想什么,越自然越好
|
||||
- `actions`:你要做的事,可以组合多个动作
|
||||
- `max_wait_seconds`:设定一个时间,对方没回的话你会再想想要不要说点什么
|
||||
- 即使什么都不想做,也放一个 `{"type": "do_nothing"}`"""
|
||||
|
||||
parts = ["## 6. 你的表达方式"]
|
||||
|
||||
if reply_style:
|
||||
parts.append(reply_style)
|
||||
|
||||
if expression_habits:
|
||||
parts.append("")
|
||||
parts.append(expression_habits)
|
||||
|
||||
parts.append("")
|
||||
parts.append(json_format)
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 组装完整的 System Prompt
|
||||
# ============================================================
|
||||
|
||||
def build_system_prompt(
|
||||
session: KokoroSession,
|
||||
available_actions: Optional[dict[str, ActionInfo]] = None,
|
||||
context_data: Optional[dict[str, str]] = None,
|
||||
chat_stream: Optional["ChatStream"] = None,
|
||||
) -> str:
|
||||
"""
|
||||
组装完整的系统提示词
|
||||
|
||||
按模块顺序组装:
|
||||
1. 角色框架(你是对话体验者)
|
||||
2. 核心身份(人设)
|
||||
3. 行为准则(规则)
|
||||
4. 情境上下文(时间/状态/关系)
|
||||
5. 动作能力(可用动作)
|
||||
6. 输出格式(表达风格 + JSON)
|
||||
|
||||
Args:
|
||||
session: 当前会话
|
||||
available_actions: 可用动作字典
|
||||
context_data: S4U 上下文数据
|
||||
chat_stream: 聊天流
|
||||
|
||||
Returns:
|
||||
完整的系统提示词
|
||||
"""
|
||||
# 角色框架 - 简洁自然
|
||||
role_frame = '你不是AI助手,你是一个真实的人,有自己的生活、想法和情绪。聊天时就像平时发微信一样自然。'
|
||||
|
||||
# 组装各模块
|
||||
modules = [
|
||||
role_frame,
|
||||
"",
|
||||
build_identity_module(),
|
||||
"",
|
||||
build_rules_module(),
|
||||
"",
|
||||
build_context_module(session, chat_stream, context_data),
|
||||
"",
|
||||
build_actions_module(available_actions),
|
||||
"",
|
||||
build_output_module(context_data),
|
||||
]
|
||||
|
||||
return "\n".join(modules)
|
||||
@@ -1,169 +0,0 @@
|
||||
"""
|
||||
KFC 响应后处理器
|
||||
|
||||
实现与全局后处理流程的集成:
|
||||
- 中文错别字生成(typo_generator)
|
||||
- 消息分割(punctuation/llm模式)
|
||||
|
||||
设计理念:复用全局配置和AFC的核心分割逻辑,与AFC保持一致的后处理行为。
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Any, Optional, TYPE_CHECKING
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import global_config
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.chat.utils.typo_generator import ChineseTypoGenerator
|
||||
|
||||
logger = get_logger("kokoro_post_processor")
|
||||
|
||||
# 延迟导入错别字生成器(避免循环导入和启动时的额外开销)
|
||||
_typo_generator: Optional["ChineseTypoGenerator"] = None
|
||||
|
||||
|
||||
def _get_typo_generator():
|
||||
"""延迟加载错别字生成器"""
|
||||
global _typo_generator
|
||||
if _typo_generator is None:
|
||||
try:
|
||||
from src.chat.utils.typo_generator import ChineseTypoGenerator
|
||||
|
||||
if global_config is None:
|
||||
logger.warning("[KFC PostProcessor] global_config 未初始化")
|
||||
return None
|
||||
|
||||
# 从全局配置读取参数
|
||||
typo_cfg = global_config.chinese_typo
|
||||
_typo_generator = ChineseTypoGenerator(
|
||||
error_rate=typo_cfg.error_rate,
|
||||
min_freq=typo_cfg.min_freq,
|
||||
tone_error_rate=typo_cfg.tone_error_rate,
|
||||
word_replace_rate=typo_cfg.word_replace_rate,
|
||||
)
|
||||
logger.info("[KFC PostProcessor] 错别字生成器已初始化")
|
||||
except Exception as e:
|
||||
logger.warning(f"[KFC PostProcessor] 初始化错别字生成器失败: {e}")
|
||||
_typo_generator = None
|
||||
return _typo_generator
|
||||
|
||||
|
||||
def split_by_punctuation(text: str, max_length: int = 256, max_sentences: int = 8) -> list[str]:
|
||||
"""
|
||||
基于标点符号分割消息 - 复用AFC的核心逻辑
|
||||
|
||||
V6修复: 不再依赖长度判断,而是直接调用AFC的分割函数
|
||||
|
||||
Args:
|
||||
text: 原始文本
|
||||
max_length: 单条消息最大长度(用于二次合并过长片段)
|
||||
max_sentences: 最大句子数
|
||||
|
||||
Returns:
|
||||
list[str]: 分割后的消息列表
|
||||
"""
|
||||
if not text:
|
||||
return []
|
||||
|
||||
# 直接复用AFC的核心分割逻辑
|
||||
from src.chat.utils.utils import split_into_sentences_w_remove_punctuation
|
||||
|
||||
# AFC的分割函数会根据标点分割并概率性合并
|
||||
sentences = split_into_sentences_w_remove_punctuation(text)
|
||||
|
||||
if not sentences:
|
||||
return [text] if text else []
|
||||
|
||||
# 限制句子数量
|
||||
if len(sentences) > max_sentences:
|
||||
sentences = sentences[:max_sentences]
|
||||
|
||||
# 如果某个片段超长,进行二次切分
|
||||
result = []
|
||||
for sentence in sentences:
|
||||
if len(sentence) > max_length:
|
||||
# 超长片段按max_length硬切分
|
||||
for i in range(0, len(sentence), max_length):
|
||||
chunk = sentence[i:i + max_length]
|
||||
if chunk.strip():
|
||||
result.append(chunk.strip())
|
||||
else:
|
||||
if sentence.strip():
|
||||
result.append(sentence.strip())
|
||||
|
||||
return result if result else [text]
|
||||
|
||||
|
||||
async def process_reply_content(content: str) -> list[str]:
|
||||
"""
|
||||
处理回复内容(主入口)
|
||||
|
||||
遵循全局配置:
|
||||
- [response_post_process].enable_response_post_process
|
||||
- [chinese_typo].enable
|
||||
- [response_splitter].enable 和 .split_mode
|
||||
|
||||
Args:
|
||||
content: 原始回复内容
|
||||
|
||||
Returns:
|
||||
list[str]: 处理后的消息列表(可能被分割成多条)
|
||||
"""
|
||||
if not content:
|
||||
return []
|
||||
|
||||
if global_config is None:
|
||||
logger.warning("[KFC PostProcessor] global_config 未初始化,返回原始内容")
|
||||
return [content]
|
||||
|
||||
# 检查全局开关
|
||||
post_process_cfg = global_config.response_post_process
|
||||
if not post_process_cfg.enable_response_post_process:
|
||||
logger.info("[KFC PostProcessor] 全局后处理已禁用,返回原始内容")
|
||||
return [content]
|
||||
|
||||
processed_content = content
|
||||
|
||||
# Step 1: 错别字生成
|
||||
typo_cfg = global_config.chinese_typo
|
||||
if typo_cfg.enable:
|
||||
try:
|
||||
typo_gen = _get_typo_generator()
|
||||
if typo_gen:
|
||||
processed_content, correction_suggestion = typo_gen.create_typo_sentence(content)
|
||||
if correction_suggestion:
|
||||
logger.info(f"[KFC PostProcessor] 生成错别字,建议纠正: {correction_suggestion}")
|
||||
else:
|
||||
logger.info("[KFC PostProcessor] 已应用错别字生成")
|
||||
except Exception as e:
|
||||
logger.warning(f"[KFC PostProcessor] 错别字生成失败: {e}")
|
||||
# 失败时使用原内容
|
||||
processed_content = content
|
||||
|
||||
# Step 2: 消息分割
|
||||
splitter_cfg = global_config.response_splitter
|
||||
if splitter_cfg.enable:
|
||||
split_mode = splitter_cfg.split_mode
|
||||
max_length = splitter_cfg.max_length
|
||||
max_sentences = splitter_cfg.max_sentence_num
|
||||
|
||||
if split_mode == "punctuation":
|
||||
# 基于标点符号分割
|
||||
result = split_by_punctuation(
|
||||
processed_content,
|
||||
max_length=max_length,
|
||||
max_sentences=max_sentences,
|
||||
)
|
||||
logger.info(f"[KFC PostProcessor] 标点分割完成,分为 {len(result)} 条消息")
|
||||
return result
|
||||
elif split_mode == "llm":
|
||||
# LLM模式:目前暂不支持,回退到不分割
|
||||
logger.info("[KFC PostProcessor] LLM分割模式暂不支持,返回完整内容")
|
||||
return [processed_content]
|
||||
else:
|
||||
logger.warning(f"[KFC PostProcessor] 未知分割模式: {split_mode}")
|
||||
return [processed_content]
|
||||
else:
|
||||
# 分割器禁用,返回完整内容
|
||||
return [processed_content]
|
||||
@@ -1,561 +0,0 @@
|
||||
"""
|
||||
Kokoro Flow Chatter 会话管理器
|
||||
|
||||
负责管理用户会话的完整生命周期:
|
||||
- 创建、加载、保存会话
|
||||
- 会话状态持久化
|
||||
- 会话清理和维护
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from src.common.logger import get_logger
|
||||
|
||||
from .models import (
|
||||
EmotionalState,
|
||||
KokoroSession,
|
||||
MentalLogEntry,
|
||||
MentalLogEventType,
|
||||
SessionStatus,
|
||||
)
|
||||
|
||||
logger = get_logger("kokoro_session_manager")
|
||||
|
||||
|
||||
class SessionManager:
|
||||
"""
|
||||
Kokoro Flow Chatter 会话管理器
|
||||
|
||||
单例模式实现,为每个私聊用户维护独立的会话
|
||||
|
||||
Features:
|
||||
- 会话的创建、获取、更新和删除
|
||||
- 自动持久化到JSON文件
|
||||
- 会话过期清理
|
||||
- 线程安全的并发访问
|
||||
"""
|
||||
|
||||
_instance: Optional["SessionManager"] = None
|
||||
_lock = asyncio.Lock()
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data_dir: str = "data/kokoro_flow_chatter/sessions",
|
||||
max_session_age_days: int = 30,
|
||||
auto_save_interval: int = 300,
|
||||
):
|
||||
"""
|
||||
初始化会话管理器
|
||||
|
||||
Args:
|
||||
data_dir: 会话数据存储目录
|
||||
max_session_age_days: 会话最大保留天数
|
||||
auto_save_interval: 自动保存间隔(秒)
|
||||
"""
|
||||
# 避免重复初始化
|
||||
if hasattr(self, "_initialized") and self._initialized:
|
||||
return
|
||||
|
||||
self._initialized = True
|
||||
self.data_dir = Path(data_dir)
|
||||
self.max_session_age_days = max_session_age_days
|
||||
self.auto_save_interval = auto_save_interval
|
||||
|
||||
# 内存中的会话缓存
|
||||
self._sessions: dict[str, KokoroSession] = {}
|
||||
self._session_locks: dict[str, asyncio.Lock] = {}
|
||||
|
||||
# 后台任务
|
||||
self._auto_save_task: Optional[asyncio.Task] = None
|
||||
self._cleanup_task: Optional[asyncio.Task] = None
|
||||
self._running = False
|
||||
|
||||
# 确保数据目录存在
|
||||
self._ensure_data_dir()
|
||||
|
||||
logger.info(f"SessionManager 初始化完成,数据目录: {self.data_dir}")
|
||||
|
||||
def _ensure_data_dir(self) -> None:
|
||||
"""确保数据目录存在"""
|
||||
self.data_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _get_session_file_path(self, user_id: str) -> Path:
|
||||
"""获取会话文件路径"""
|
||||
# 清理user_id中的特殊字符
|
||||
safe_user_id = "".join(c if c.isalnum() or c in "-_" else "_" for c in user_id)
|
||||
return self.data_dir / f"{safe_user_id}.json"
|
||||
|
||||
async def _get_session_lock(self, user_id: str) -> asyncio.Lock:
|
||||
"""获取会话级别的锁"""
|
||||
if user_id not in self._session_locks:
|
||||
self._session_locks[user_id] = asyncio.Lock()
|
||||
return self._session_locks[user_id]
|
||||
|
||||
async def start(self) -> None:
|
||||
"""启动会话管理器的后台任务"""
|
||||
if self._running:
|
||||
return
|
||||
|
||||
self._running = True
|
||||
|
||||
# 启动自动保存任务
|
||||
self._auto_save_task = asyncio.create_task(self._auto_save_loop())
|
||||
|
||||
# 启动清理任务
|
||||
self._cleanup_task = asyncio.create_task(self._cleanup_loop())
|
||||
|
||||
logger.info("SessionManager 后台任务已启动")
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""停止会话管理器并保存所有会话"""
|
||||
self._running = False
|
||||
|
||||
# 取消后台任务
|
||||
if self._auto_save_task:
|
||||
self._auto_save_task.cancel()
|
||||
try:
|
||||
await self._auto_save_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
if self._cleanup_task:
|
||||
self._cleanup_task.cancel()
|
||||
try:
|
||||
await self._cleanup_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
# 保存所有会话
|
||||
await self.save_all_sessions()
|
||||
|
||||
logger.info("SessionManager 已停止,所有会话已保存")
|
||||
|
||||
async def _auto_save_loop(self) -> None:
|
||||
"""自动保存循环"""
|
||||
while self._running:
|
||||
try:
|
||||
await asyncio.sleep(self.auto_save_interval)
|
||||
await self.save_all_sessions()
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"自动保存会话时出错: {e}")
|
||||
|
||||
async def _cleanup_loop(self) -> None:
|
||||
"""清理过期会话循环"""
|
||||
while self._running:
|
||||
try:
|
||||
# 每小时清理一次
|
||||
await asyncio.sleep(3600)
|
||||
await self.cleanup_expired_sessions()
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"清理过期会话时出错: {e}")
|
||||
|
||||
async def get_session(self, user_id: str, stream_id: str) -> KokoroSession:
|
||||
"""
|
||||
获取或创建用户会话
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
stream_id: 聊天流ID
|
||||
|
||||
Returns:
|
||||
KokoroSession: 用户会话对象
|
||||
"""
|
||||
lock = await self._get_session_lock(user_id)
|
||||
async with lock:
|
||||
# 检查内存缓存
|
||||
if user_id in self._sessions:
|
||||
session = self._sessions[user_id]
|
||||
# 更新stream_id(可能发生变化)
|
||||
session.stream_id = stream_id
|
||||
return session
|
||||
|
||||
# 尝试从文件加载
|
||||
session = await self._load_session_from_file(user_id)
|
||||
if session:
|
||||
session.stream_id = stream_id
|
||||
self._sessions[user_id] = session
|
||||
logger.debug(f"从文件加载会话: {user_id}")
|
||||
return session
|
||||
|
||||
# 创建新会话
|
||||
session = KokoroSession(
|
||||
user_id=user_id,
|
||||
stream_id=stream_id,
|
||||
status=SessionStatus.IDLE,
|
||||
emotional_state=EmotionalState(),
|
||||
mental_log=[],
|
||||
)
|
||||
|
||||
# 添加初始日志条目
|
||||
initial_entry = MentalLogEntry(
|
||||
event_type=MentalLogEventType.STATE_CHANGE,
|
||||
timestamp=time.time(),
|
||||
thought="与这位用户的对话开始了,我对接下来的交流充满期待。",
|
||||
content="会话创建",
|
||||
emotional_snapshot=session.emotional_state.to_dict(),
|
||||
)
|
||||
session.add_mental_log_entry(initial_entry)
|
||||
|
||||
self._sessions[user_id] = session
|
||||
logger.info(f"创建新会话: {user_id}")
|
||||
|
||||
return session
|
||||
|
||||
async def _load_session_from_file(self, user_id: str) -> Optional[KokoroSession]:
|
||||
"""从文件加载会话"""
|
||||
file_path = self._get_session_file_path(user_id)
|
||||
|
||||
if not file_path.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
session = KokoroSession.from_dict(data)
|
||||
|
||||
# V7: 情绪健康检查 - 防止从持久化数据恢复无厘头的负面情绪
|
||||
session = self._sanitize_emotional_state(session)
|
||||
|
||||
logger.debug(f"成功从文件加载会话: {user_id}")
|
||||
return session
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"解析会话文件失败 {user_id}: {e}")
|
||||
# 备份损坏的文件
|
||||
backup_path = file_path.with_suffix(".json.bak")
|
||||
os.rename(file_path, backup_path)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"加载会话文件失败 {user_id}: {e}")
|
||||
return None
|
||||
|
||||
def _sanitize_emotional_state(self, session: KokoroSession) -> KokoroSession:
|
||||
"""
|
||||
V7: 情绪健康检查
|
||||
|
||||
检查并修正不合理的情绪状态,防止:
|
||||
1. 无厘头的负面情绪从持久化数据恢复
|
||||
2. 情绪强度过高(>0.8)的负面情绪
|
||||
3. 长时间未更新的情绪状态
|
||||
|
||||
Args:
|
||||
session: 会话对象
|
||||
|
||||
Returns:
|
||||
修正后的会话对象
|
||||
"""
|
||||
emotional_state = session.emotional_state
|
||||
current_mood = emotional_state.mood.lower() if emotional_state.mood else ""
|
||||
|
||||
# 负面情绪关键词列表
|
||||
negative_moods = [
|
||||
"低落", "沮丧", "难过", "伤心", "失落", "郁闷", "烦躁", "焦虑",
|
||||
"担忧", "害怕", "恐惧", "愤怒", "生气", "不安", "忧郁", "悲伤",
|
||||
"sad", "depressed", "anxious", "angry", "upset", "worried"
|
||||
]
|
||||
|
||||
is_negative = any(neg in current_mood for neg in negative_moods)
|
||||
|
||||
# 检查1: 如果是负面情绪且强度较高(>0.6),重置为平静
|
||||
if is_negative and emotional_state.mood_intensity > 0.6:
|
||||
logger.warning(
|
||||
f"[KFC] 检测到高强度负面情绪 ({emotional_state.mood}, {emotional_state.mood_intensity:.1%}),"
|
||||
f"重置为平静状态"
|
||||
)
|
||||
emotional_state.mood = "平静"
|
||||
emotional_state.mood_intensity = 0.3
|
||||
|
||||
# 检查2: 如果情绪超过24小时未更新,重置为平静
|
||||
import time as time_module
|
||||
time_since_update = time_module.time() - emotional_state.last_update_time
|
||||
if time_since_update > 86400: # 24小时 = 86400秒
|
||||
logger.info(
|
||||
f"[KFC] 情绪状态超过24小时未更新 ({time_since_update/3600:.1f}h),"
|
||||
f"重置为平静状态"
|
||||
)
|
||||
emotional_state.mood = "平静"
|
||||
emotional_state.mood_intensity = 0.3
|
||||
emotional_state.anxiety_level = 0.0
|
||||
emotional_state.last_update_time = time_module.time()
|
||||
|
||||
# 检查3: 焦虑程度过高也需要重置
|
||||
if emotional_state.anxiety_level > 0.8:
|
||||
logger.info(f"[KFC] 焦虑程度过高 ({emotional_state.anxiety_level:.1%}),重置为正常")
|
||||
emotional_state.anxiety_level = 0.3
|
||||
|
||||
return session
|
||||
|
||||
async def save_session(self, user_id: str) -> bool:
|
||||
"""
|
||||
保存单个会话到文件
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
bool: 是否保存成功
|
||||
"""
|
||||
lock = await self._get_session_lock(user_id)
|
||||
async with lock:
|
||||
if user_id not in self._sessions:
|
||||
return False
|
||||
|
||||
session = self._sessions[user_id]
|
||||
file_path = self._get_session_file_path(user_id)
|
||||
|
||||
try:
|
||||
data = session.to_dict()
|
||||
|
||||
# 先写入临时文件,再重命名(原子操作)
|
||||
temp_path = file_path.with_suffix(".json.tmp")
|
||||
with open(temp_path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
os.replace(temp_path, file_path)
|
||||
logger.debug(f"保存会话成功: {user_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"保存会话失败 {user_id}: {e}")
|
||||
return False
|
||||
|
||||
async def save_all_sessions(self) -> int:
|
||||
"""
|
||||
保存所有会话
|
||||
|
||||
Returns:
|
||||
int: 成功保存的会话数量
|
||||
"""
|
||||
saved_count = 0
|
||||
for user_id in list(self._sessions.keys()):
|
||||
if await self.save_session(user_id):
|
||||
saved_count += 1
|
||||
|
||||
if saved_count > 0:
|
||||
logger.debug(f"批量保存完成,共保存 {saved_count} 个会话")
|
||||
|
||||
return saved_count
|
||||
|
||||
async def update_session(
|
||||
self,
|
||||
user_id: str,
|
||||
status: Optional[SessionStatus] = None,
|
||||
emotional_state: Optional[EmotionalState] = None,
|
||||
mental_log_entry: Optional[MentalLogEntry] = None,
|
||||
**kwargs,
|
||||
) -> bool:
|
||||
"""
|
||||
更新会话状态
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
status: 新的会话状态
|
||||
emotional_state: 新的情感状态
|
||||
mental_log_entry: 要添加的心理日志条目
|
||||
**kwargs: 其他要更新的字段
|
||||
|
||||
Returns:
|
||||
bool: 是否更新成功
|
||||
"""
|
||||
lock = await self._get_session_lock(user_id)
|
||||
async with lock:
|
||||
if user_id not in self._sessions:
|
||||
return False
|
||||
|
||||
session = self._sessions[user_id]
|
||||
|
||||
if status is not None:
|
||||
old_status = session.status
|
||||
session.status = status
|
||||
logger.debug(f"会话状态变更 {user_id}: {old_status} -> {status}")
|
||||
|
||||
if emotional_state is not None:
|
||||
session.emotional_state = emotional_state
|
||||
|
||||
if mental_log_entry is not None:
|
||||
session.add_mental_log_entry(mental_log_entry)
|
||||
|
||||
# 更新其他字段
|
||||
for key, value in kwargs.items():
|
||||
if hasattr(session, key):
|
||||
setattr(session, key, value)
|
||||
|
||||
session.last_activity_at = time.time()
|
||||
|
||||
return True
|
||||
|
||||
async def delete_session(self, user_id: str) -> bool:
|
||||
"""
|
||||
删除会话
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
bool: 是否删除成功
|
||||
"""
|
||||
lock = await self._get_session_lock(user_id)
|
||||
async with lock:
|
||||
# 从内存中删除
|
||||
if user_id in self._sessions:
|
||||
del self._sessions[user_id]
|
||||
|
||||
# 从文件系统删除
|
||||
file_path = self._get_session_file_path(user_id)
|
||||
if file_path.exists():
|
||||
try:
|
||||
os.remove(file_path)
|
||||
logger.info(f"删除会话: {user_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"删除会话文件失败 {user_id}: {e}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
async def cleanup_expired_sessions(self) -> int:
|
||||
"""
|
||||
清理过期会话
|
||||
|
||||
Returns:
|
||||
int: 清理的会话数量
|
||||
"""
|
||||
cleaned_count = 0
|
||||
current_time = time.time()
|
||||
max_age_seconds = self.max_session_age_days * 24 * 3600
|
||||
|
||||
# 检查文件系统中的所有会话
|
||||
for file_path in self.data_dir.glob("*.json"):
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
last_activity = data.get("last_activity_at", 0)
|
||||
if current_time - last_activity > max_age_seconds:
|
||||
user_id = data.get("user_id", file_path.stem)
|
||||
|
||||
# 从内存中删除
|
||||
if user_id in self._sessions:
|
||||
del self._sessions[user_id]
|
||||
|
||||
# 删除文件
|
||||
os.remove(file_path)
|
||||
cleaned_count += 1
|
||||
logger.info(f"清理过期会话: {user_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"清理会话时出错 {file_path}: {e}")
|
||||
|
||||
if cleaned_count > 0:
|
||||
logger.info(f"共清理 {cleaned_count} 个过期会话")
|
||||
|
||||
return cleaned_count
|
||||
|
||||
async def get_all_waiting_sessions(self) -> list[KokoroSession]:
|
||||
"""
|
||||
获取所有处于等待状态的会话
|
||||
|
||||
Returns:
|
||||
list[KokoroSession]: 等待中的会话列表
|
||||
"""
|
||||
waiting_sessions = []
|
||||
|
||||
for session in self._sessions.values():
|
||||
if session.status == SessionStatus.WAITING:
|
||||
waiting_sessions.append(session)
|
||||
|
||||
return waiting_sessions
|
||||
|
||||
async def get_all_sessions(self) -> list[KokoroSession]:
|
||||
"""
|
||||
获取所有内存中的会话
|
||||
|
||||
用于主动思考检查等需要遍历所有会话的场景
|
||||
|
||||
Returns:
|
||||
list[KokoroSession]: 所有会话列表
|
||||
"""
|
||||
return list(self._sessions.values())
|
||||
|
||||
async def get_session_statistics(self) -> dict:
|
||||
"""
|
||||
获取会话统计信息
|
||||
|
||||
Returns:
|
||||
dict: 统计信息字典
|
||||
"""
|
||||
total_in_memory = len(self._sessions)
|
||||
status_counts = {}
|
||||
|
||||
for session in self._sessions.values():
|
||||
status = str(session.status)
|
||||
status_counts[status] = status_counts.get(status, 0) + 1
|
||||
|
||||
# 统计文件系统中的会话
|
||||
total_on_disk = len(list(self.data_dir.glob("*.json")))
|
||||
|
||||
return {
|
||||
"total_in_memory": total_in_memory,
|
||||
"total_on_disk": total_on_disk,
|
||||
"status_counts": status_counts,
|
||||
"data_directory": str(self.data_dir),
|
||||
}
|
||||
|
||||
def get_session_sync(self, user_id: str) -> Optional[KokoroSession]:
|
||||
"""
|
||||
同步获取会话(仅从内存缓存)
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
Optional[KokoroSession]: 会话对象,如果不存在返回None
|
||||
"""
|
||||
return self._sessions.get(user_id)
|
||||
|
||||
|
||||
# 全局会话管理器实例
|
||||
_session_manager: Optional[SessionManager] = None
|
||||
|
||||
|
||||
def get_session_manager() -> SessionManager:
|
||||
"""获取全局会话管理器实例"""
|
||||
global _session_manager
|
||||
if _session_manager is None:
|
||||
_session_manager = SessionManager()
|
||||
return _session_manager
|
||||
|
||||
|
||||
async def initialize_session_manager(
|
||||
data_dir: str = "data/kokoro_flow_chatter/sessions",
|
||||
**kwargs,
|
||||
) -> SessionManager:
|
||||
"""
|
||||
初始化并启动会话管理器
|
||||
|
||||
Args:
|
||||
data_dir: 数据存储目录
|
||||
**kwargs: 其他配置参数
|
||||
|
||||
Returns:
|
||||
SessionManager: 会话管理器实例
|
||||
"""
|
||||
global _session_manager
|
||||
_session_manager = SessionManager(data_dir=data_dir, **kwargs)
|
||||
await _session_manager.start()
|
||||
return _session_manager
|
||||
@@ -20,7 +20,6 @@ from .models import (
|
||||
from .session import KokoroSession, SessionManager, get_session_manager
|
||||
from .chatter import KokoroFlowChatterV2
|
||||
from .replyer import generate_response
|
||||
from .action_executor import ActionExecutor
|
||||
from .proactive_thinker import (
|
||||
ProactiveThinker,
|
||||
get_proactive_thinker,
|
||||
@@ -50,7 +49,6 @@ __all__ = [
|
||||
# Core Components
|
||||
"KokoroFlowChatterV2",
|
||||
"generate_response",
|
||||
"ActionExecutor",
|
||||
# Proactive Thinker
|
||||
"ProactiveThinker",
|
||||
"get_proactive_thinker",
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
"""
|
||||
Kokoro Flow Chatter V2 - 动作执行器
|
||||
|
||||
负责执行 LLM 决策的动作
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
from src.chat.planner_actions.action_manager import ChatterActionManager
|
||||
from src.common.logger import get_logger
|
||||
from src.plugin_system.apis import send_api
|
||||
|
||||
from .models import ActionModel, LLMResponse
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.chat.message_receive.chat_stream import ChatStream
|
||||
|
||||
logger = get_logger("kfc_v2_action_executor")
|
||||
|
||||
|
||||
class ActionExecutor:
|
||||
"""
|
||||
动作执行器
|
||||
|
||||
职责:
|
||||
- 执行 reply、poke_user 等动作
|
||||
- 通过 ActionManager 执行动态注册的动作
|
||||
"""
|
||||
|
||||
# 内置动作(不通过 ActionManager)
|
||||
BUILTIN_ACTIONS = {"reply", "do_nothing"}
|
||||
|
||||
def __init__(self, stream_id: str):
|
||||
self.stream_id = stream_id
|
||||
self._action_manager = ChatterActionManager()
|
||||
self._available_actions: dict = {}
|
||||
|
||||
# 统计
|
||||
self._stats = {
|
||||
"total_executed": 0,
|
||||
"successful": 0,
|
||||
"failed": 0,
|
||||
}
|
||||
|
||||
async def load_actions(self) -> dict:
|
||||
"""加载可用动作"""
|
||||
await self._action_manager.load_actions(self.stream_id)
|
||||
self._available_actions = self._action_manager.get_using_actions()
|
||||
logger.debug(f"[ActionExecutor] 加载了 {len(self._available_actions)} 个动作")
|
||||
return self._available_actions
|
||||
|
||||
def get_available_actions(self) -> dict:
|
||||
"""获取可用动作"""
|
||||
return self._available_actions.copy()
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
response: LLMResponse,
|
||||
chat_stream: Optional["ChatStream"],
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
执行动作列表
|
||||
|
||||
Args:
|
||||
response: LLM 响应
|
||||
chat_stream: 聊天流
|
||||
|
||||
Returns:
|
||||
执行结果
|
||||
"""
|
||||
results = []
|
||||
has_reply = False
|
||||
reply_content = ""
|
||||
|
||||
for action in response.actions:
|
||||
try:
|
||||
result = await self._execute_action(action, chat_stream)
|
||||
results.append(result)
|
||||
|
||||
if result.get("success"):
|
||||
self._stats["successful"] += 1
|
||||
if action.type in ("reply", "respond"):
|
||||
has_reply = True
|
||||
reply_content = action.params.get("content", "")
|
||||
else:
|
||||
self._stats["failed"] += 1
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[ActionExecutor] 执行动作失败 {action.type}: {e}")
|
||||
results.append({
|
||||
"action_type": action.type,
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
})
|
||||
self._stats["failed"] += 1
|
||||
|
||||
self._stats["total_executed"] += 1
|
||||
|
||||
return {
|
||||
"success": all(r.get("success", False) for r in results),
|
||||
"results": results,
|
||||
"has_reply": has_reply,
|
||||
"reply_content": reply_content,
|
||||
}
|
||||
|
||||
async def _execute_action(
|
||||
self,
|
||||
action: ActionModel,
|
||||
chat_stream: Optional["ChatStream"],
|
||||
) -> dict[str, Any]:
|
||||
"""执行单个动作"""
|
||||
action_type = action.type
|
||||
|
||||
if action_type == "reply":
|
||||
return await self._execute_reply(action, chat_stream)
|
||||
|
||||
elif action_type == "do_nothing":
|
||||
logger.debug("[ActionExecutor] 执行 do_nothing")
|
||||
return {"action_type": "do_nothing", "success": True}
|
||||
|
||||
elif action_type == "poke_user":
|
||||
return await self._execute_via_manager(action, chat_stream)
|
||||
|
||||
elif action_type in self._available_actions:
|
||||
return await self._execute_via_manager(action, chat_stream)
|
||||
|
||||
else:
|
||||
logger.warning(f"[ActionExecutor] 未知动作类型: {action_type}")
|
||||
return {
|
||||
"action_type": action_type,
|
||||
"success": False,
|
||||
"error": f"未知动作类型: {action_type}",
|
||||
}
|
||||
|
||||
async def _execute_reply(
|
||||
self,
|
||||
action: ActionModel,
|
||||
chat_stream: Optional["ChatStream"],
|
||||
) -> dict[str, Any]:
|
||||
"""执行回复动作"""
|
||||
content = action.params.get("content", "")
|
||||
|
||||
if not content:
|
||||
return {
|
||||
"action_type": "reply",
|
||||
"success": False,
|
||||
"error": "回复内容为空",
|
||||
}
|
||||
|
||||
try:
|
||||
# 消息后处理(分割、错别字等)
|
||||
processed_messages = await self._process_reply_content(content)
|
||||
|
||||
all_success = True
|
||||
for msg in processed_messages:
|
||||
success = await send_api.text_to_stream(
|
||||
text=msg,
|
||||
stream_id=self.stream_id,
|
||||
typing=True,
|
||||
)
|
||||
if not success:
|
||||
all_success = False
|
||||
|
||||
return {
|
||||
"action_type": "reply",
|
||||
"success": all_success,
|
||||
"reply_text": content,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[ActionExecutor] 发送回复失败: {e}")
|
||||
return {
|
||||
"action_type": "reply",
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
async def _process_reply_content(self, content: str) -> list[str]:
|
||||
"""处理回复内容(分割、错别字等)"""
|
||||
try:
|
||||
# 复用 v1 的后处理器
|
||||
from src.plugins.built_in.kokoro_flow_chatter.response_post_processor import (
|
||||
process_reply_content,
|
||||
)
|
||||
|
||||
messages = await process_reply_content(content)
|
||||
return messages if messages else [content]
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"[ActionExecutor] 消息处理失败,使用原始内容: {e}")
|
||||
return [content]
|
||||
|
||||
async def _execute_via_manager(
|
||||
self,
|
||||
action: ActionModel,
|
||||
chat_stream: Optional["ChatStream"],
|
||||
) -> dict[str, Any]:
|
||||
"""通过 ActionManager 执行动作"""
|
||||
try:
|
||||
result = await self._action_manager.execute_action(
|
||||
action_name=action.type,
|
||||
chat_id=self.stream_id,
|
||||
target_message=None,
|
||||
reasoning=f"KFC决策: {action.type}",
|
||||
action_data=action.params,
|
||||
thinking_id=None,
|
||||
log_prefix="[KFC V2]",
|
||||
)
|
||||
|
||||
return {
|
||||
"action_type": action.type,
|
||||
"success": result.get("success", False),
|
||||
"result": result,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[ActionExecutor] ActionManager 执行失败: {e}")
|
||||
return {
|
||||
"action_type": action.type,
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
def get_stats(self) -> dict:
|
||||
"""获取统计信息"""
|
||||
return self._stats.copy()
|
||||
@@ -10,21 +10,20 @@ Kokoro Flow Chatter V2 - Chatter 主类
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Optional
|
||||
from typing import TYPE_CHECKING, Any, ClassVar
|
||||
|
||||
from src.chat.planner_actions.action_manager import ChatterActionManager
|
||||
from src.common.data_models.message_manager_data_model import StreamContext
|
||||
from src.common.logger import get_logger
|
||||
from src.plugin_system.base.base_chatter import BaseChatter
|
||||
from src.plugin_system.base.component_types import ChatType
|
||||
|
||||
from .action_executor import ActionExecutor
|
||||
from .models import EventType, SessionStatus
|
||||
from .models import SessionStatus
|
||||
from .replyer import generate_response
|
||||
from .session import get_session_manager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.chat.planner_actions.action_manager import ChatterActionManager
|
||||
from src.common.data_models.database_data_model import DatabaseMessages
|
||||
pass
|
||||
|
||||
logger = get_logger("kfc_v2_chatter")
|
||||
|
||||
@@ -62,7 +61,6 @@ class KokoroFlowChatterV2(BaseChatter):
|
||||
|
||||
# 核心组件
|
||||
self.session_manager = get_session_manager()
|
||||
self.action_executor = ActionExecutor(stream_id)
|
||||
|
||||
# 并发控制
|
||||
self._lock = asyncio.Lock()
|
||||
@@ -129,9 +127,12 @@ class KokoroFlowChatterV2(BaseChatter):
|
||||
timestamp=msg.time,
|
||||
)
|
||||
|
||||
# 6. 加载可用动作
|
||||
await self.action_executor.load_actions()
|
||||
available_actions = self.action_executor.get_available_actions()
|
||||
# 6. 加载可用动作(通过 ActionModifier 过滤)
|
||||
from src.chat.planner_actions.action_modifier import ActionModifier
|
||||
|
||||
action_modifier = ActionModifier(self.action_manager, self.stream_id)
|
||||
await action_modifier.modify_actions(chatter_name="KokoroFlowChatterV2")
|
||||
available_actions = self.action_manager.get_using_actions()
|
||||
|
||||
# 7. 获取聊天流
|
||||
chat_stream = await self._get_chat_stream()
|
||||
@@ -146,7 +147,21 @@ class KokoroFlowChatterV2(BaseChatter):
|
||||
)
|
||||
|
||||
# 9. 执行动作
|
||||
exec_result = await self.action_executor.execute(response, chat_stream)
|
||||
exec_results = []
|
||||
has_reply = False
|
||||
for action in response.actions:
|
||||
result = await self.action_manager.execute_action(
|
||||
action_name=action.type,
|
||||
chat_id=self.stream_id,
|
||||
target_message=target_message,
|
||||
reasoning=response.thought,
|
||||
action_data=action.params,
|
||||
thinking_id=None,
|
||||
log_prefix="[KFC V2]",
|
||||
)
|
||||
exec_results.append(result)
|
||||
if result.get("success") and action.type in ("reply", "respond"):
|
||||
has_reply = True
|
||||
|
||||
# 10. 记录 Bot 规划到 mental_log
|
||||
session.add_bot_planning(
|
||||
@@ -174,7 +189,7 @@ class KokoroFlowChatterV2(BaseChatter):
|
||||
|
||||
# 14. 更新统计
|
||||
self._stats["messages_processed"] += len(unread_messages)
|
||||
if exec_result.get("has_reply"):
|
||||
if has_reply:
|
||||
self._stats["successful_responses"] += 1
|
||||
|
||||
logger.info(
|
||||
@@ -187,7 +202,7 @@ class KokoroFlowChatterV2(BaseChatter):
|
||||
return self._build_result(
|
||||
success=True,
|
||||
message="processed",
|
||||
has_reply=exec_result.get("has_reply", False),
|
||||
has_reply=has_reply,
|
||||
thought=response.thought,
|
||||
situation_type=situation_type,
|
||||
)
|
||||
@@ -252,10 +267,7 @@ class KokoroFlowChatterV2(BaseChatter):
|
||||
|
||||
def get_stats(self) -> dict[str, Any]:
|
||||
"""获取统计信息"""
|
||||
return {
|
||||
**self._stats,
|
||||
"action_executor_stats": self.action_executor.get_stats(),
|
||||
}
|
||||
return self._stats.copy()
|
||||
|
||||
@property
|
||||
def is_processing(self) -> bool:
|
||||
|
||||
@@ -15,11 +15,11 @@ import time
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any, Callable, Coroutine, Optional
|
||||
|
||||
from src.chat.planner_actions.action_manager import ChatterActionManager
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import global_config
|
||||
from src.plugin_system.apis.unified_scheduler import TriggerType, unified_scheduler
|
||||
|
||||
from .action_executor import ActionExecutor
|
||||
from .models import EventType, SessionStatus
|
||||
from .replyer import generate_response
|
||||
from .session import KokoroSession, get_session_manager
|
||||
@@ -257,8 +257,8 @@ class ProactiveThinker:
|
||||
chat_stream = await self._get_chat_stream(session.stream_id)
|
||||
|
||||
# 加载动作
|
||||
action_executor = ActionExecutor(session.stream_id)
|
||||
await action_executor.load_actions()
|
||||
action_manager = ChatterActionManager()
|
||||
await action_manager.load_actions(session.stream_id)
|
||||
|
||||
# 调用 Replyer 生成超时决策
|
||||
response = await generate_response(
|
||||
@@ -266,11 +266,20 @@ class ProactiveThinker:
|
||||
user_name=session.user_id, # 这里可以改进,获取真实用户名
|
||||
situation_type="timeout",
|
||||
chat_stream=chat_stream,
|
||||
available_actions=action_executor.get_available_actions(),
|
||||
available_actions=action_manager.get_using_actions(),
|
||||
)
|
||||
|
||||
# 执行动作
|
||||
exec_result = await action_executor.execute(response, chat_stream)
|
||||
for action in response.actions:
|
||||
await action_manager.execute_action(
|
||||
action_name=action.type,
|
||||
chat_id=session.stream_id,
|
||||
target_message=None,
|
||||
reasoning=response.thought,
|
||||
action_data=action.params,
|
||||
thinking_id=None,
|
||||
log_prefix="[KFC V2 ProactiveThinker]",
|
||||
)
|
||||
|
||||
# 记录到 mental_log
|
||||
session.add_bot_planning(
|
||||
@@ -389,8 +398,8 @@ class ProactiveThinker:
|
||||
chat_stream = await self._get_chat_stream(session.stream_id)
|
||||
|
||||
# 加载动作
|
||||
action_executor = ActionExecutor(session.stream_id)
|
||||
await action_executor.load_actions()
|
||||
action_manager = ChatterActionManager()
|
||||
await action_manager.load_actions(session.stream_id)
|
||||
|
||||
# 计算沉默时长
|
||||
silence_seconds = time.time() - session.last_activity_at
|
||||
@@ -405,7 +414,7 @@ class ProactiveThinker:
|
||||
user_name=session.user_id,
|
||||
situation_type="proactive",
|
||||
chat_stream=chat_stream,
|
||||
available_actions=action_executor.get_available_actions(),
|
||||
available_actions=action_manager.get_using_actions(),
|
||||
extra_context={
|
||||
"trigger_reason": trigger_reason,
|
||||
"silence_duration": silence_duration,
|
||||
@@ -425,7 +434,16 @@ class ProactiveThinker:
|
||||
return
|
||||
|
||||
# 执行动作
|
||||
exec_result = await action_executor.execute(response, chat_stream)
|
||||
for action in response.actions:
|
||||
await action_manager.execute_action(
|
||||
action_name=action.type,
|
||||
chat_id=session.stream_id,
|
||||
target_message=None,
|
||||
reasoning=response.thought,
|
||||
action_data=action.params,
|
||||
thinking_id=None,
|
||||
log_prefix="[KFC V2 ProactiveThinker]",
|
||||
)
|
||||
|
||||
# 记录到 mental_log
|
||||
session.add_bot_planning(
|
||||
|
||||
Reference in New Issue
Block a user