feat(KFC): 更新聊天处理器和回复模块,优化动作名称及上下文构建逻辑
This commit is contained in:
@@ -57,12 +57,40 @@ class ChatterManager:
|
||||
|
||||
self.stats["chatters_registered"] += 1
|
||||
|
||||
def get_chatter_class(self, chat_type: ChatType) -> type | None:
|
||||
"""获取指定聊天类型的聊天处理器类"""
|
||||
if chat_type in self.chatter_classes:
|
||||
return self.chatter_classes[chat_type][0]
|
||||
def get_chatter_class_for_chat_type(self, chat_type: ChatType) -> type | None:
|
||||
"""
|
||||
获取指定聊天类型的最佳聊天处理器类
|
||||
|
||||
优先级规则:
|
||||
1. 优先选择明确匹配当前聊天类型的 Chatter(如 PRIVATE 或 GROUP)
|
||||
2. 如果没有精确匹配,才使用 ALL 类型的 Chatter
|
||||
|
||||
Args:
|
||||
chat_type: 聊天类型
|
||||
|
||||
Returns:
|
||||
最佳匹配的聊天处理器类,如果没有匹配则返回 None
|
||||
"""
|
||||
# 1. 首先尝试精确匹配(排除 ALL 类型)
|
||||
if chat_type != ChatType.ALL and chat_type in self.chatter_classes:
|
||||
chatter_list = self.chatter_classes[chat_type]
|
||||
if chatter_list:
|
||||
logger.debug(f"找到精确匹配的聊天处理器: {chatter_list[0].__name__} for {chat_type.value}")
|
||||
return chatter_list[0]
|
||||
|
||||
# 2. 如果没有精确匹配,回退到 ALL 类型
|
||||
if ChatType.ALL in self.chatter_classes:
|
||||
chatter_list = self.chatter_classes[ChatType.ALL]
|
||||
if chatter_list:
|
||||
logger.debug(f"使用通用聊天处理器: {chatter_list[0].__name__} for {chat_type.value}")
|
||||
return chatter_list[0]
|
||||
|
||||
return None
|
||||
|
||||
def get_chatter_class(self, chat_type: ChatType) -> type | None:
|
||||
"""获取指定聊天类型的聊天处理器类(兼容旧接口)"""
|
||||
return self.get_chatter_class_for_chat_type(chat_type)
|
||||
|
||||
def get_supported_chat_types(self) -> list[ChatType]:
|
||||
"""获取支持的聊天类型列表"""
|
||||
return list(self.chatter_classes.keys())
|
||||
@@ -112,29 +140,29 @@ class ChatterManager:
|
||||
logger.error("schedule unread cleanup failed", stream_id=stream_id, error=runtime_error)
|
||||
|
||||
async def process_stream_context(self, stream_id: str, context: "StreamContext") -> dict:
|
||||
"""处理流上下文"""
|
||||
"""
|
||||
处理流上下文
|
||||
|
||||
每个聊天流只能有一个活跃的 Chatter 组件。
|
||||
选择优先级:明确指定聊天类型的 Chatter > ALL 类型的 Chatter
|
||||
"""
|
||||
chat_type = context.chat_type
|
||||
chat_type_value = chat_type.value
|
||||
logger.debug("处理流上下文", stream_id=stream_id, chat_type=chat_type_value)
|
||||
|
||||
self._ensure_chatter_registry()
|
||||
|
||||
chatter_class = self.get_chatter_class(chat_type)
|
||||
if not chatter_class:
|
||||
all_chatter_class = self.get_chatter_class(ChatType.ALL)
|
||||
if all_chatter_class:
|
||||
chatter_class = all_chatter_class
|
||||
logger.info(
|
||||
"回退到通用聊天处理器",
|
||||
stream_id=stream_id,
|
||||
requested_type=chat_type_value,
|
||||
fallback=ChatType.ALL.value,
|
||||
)
|
||||
else:
|
||||
# 检查是否已有该流的 Chatter 实例
|
||||
stream_instance = self.instances.get(stream_id)
|
||||
|
||||
if stream_instance is None:
|
||||
# 使用新的优先级选择逻辑获取最佳 Chatter 类
|
||||
chatter_class = self.get_chatter_class_for_chat_type(chat_type)
|
||||
|
||||
if not chatter_class:
|
||||
raise ValueError(f"No chatter registered for chat type {chat_type}")
|
||||
|
||||
stream_instance = self.instances.get(stream_id)
|
||||
if stream_instance is None:
|
||||
# 创建新实例
|
||||
stream_instance = chatter_class(stream_id=stream_id, action_manager=self.action_manager)
|
||||
self.instances[stream_id] = stream_instance
|
||||
logger.info(
|
||||
@@ -143,6 +171,13 @@ class ChatterManager:
|
||||
chatter_class=chatter_class.__name__,
|
||||
chat_type=chat_type_value,
|
||||
)
|
||||
else:
|
||||
# 已有实例,直接使用(每个流只有一个活跃的 Chatter)
|
||||
logger.debug(
|
||||
"使用已有聊天处理器实例",
|
||||
stream_id=stream_id,
|
||||
chatter_class=stream_instance.__class__.__name__,
|
||||
)
|
||||
|
||||
self.stats["streams_processed"] += 1
|
||||
try:
|
||||
|
||||
@@ -22,10 +22,12 @@ class KFCReplyAction(BaseAction):
|
||||
- 不调用 LLM,直接发送 content 参数中的内容
|
||||
- content 由 Replyer 提前生成
|
||||
- 仅限 KokoroFlowChatterV2 使用
|
||||
|
||||
注意:使用 kfc_reply 作为动作名称以避免与 AFC 的 reply 动作冲突
|
||||
"""
|
||||
|
||||
# 动作基本信息
|
||||
action_name = "reply"
|
||||
action_name = "kfc_reply"
|
||||
action_description = "发送回复消息。content 参数包含要发送的内容。"
|
||||
|
||||
# 激活设置
|
||||
|
||||
@@ -160,7 +160,7 @@ class KokoroFlowChatterV2(BaseChatter):
|
||||
log_prefix="[KFC V2]",
|
||||
)
|
||||
exec_results.append(result)
|
||||
if result.get("success") and action.type in ("reply", "respond"):
|
||||
if result.get("success") and action.type in ("kfc_reply", "respond"):
|
||||
has_reply = True
|
||||
|
||||
# 10. 记录 Bot 规划到 mental_log
|
||||
|
||||
@@ -50,6 +50,7 @@ class KFCContextBuilder:
|
||||
sender_name: str,
|
||||
target_message: str,
|
||||
context: Optional["StreamContext"] = None,
|
||||
user_id: Optional[str] = None,
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
并行构建所有上下文模块
|
||||
@@ -58,6 +59,7 @@ class KFCContextBuilder:
|
||||
sender_name: 发送者名称
|
||||
target_message: 目标消息内容
|
||||
context: 聊天流上下文(可选)
|
||||
user_id: 用户ID(可选,用于精确查找关系信息)
|
||||
|
||||
Returns:
|
||||
dict: 包含所有上下文块的字典
|
||||
@@ -65,7 +67,7 @@ class KFCContextBuilder:
|
||||
chat_history = await self._get_chat_history_text(context)
|
||||
|
||||
tasks = {
|
||||
"relation_info": self._build_relation_info(sender_name, target_message),
|
||||
"relation_info": self._build_relation_info(sender_name, target_message, user_id),
|
||||
"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(),
|
||||
@@ -127,7 +129,7 @@ class KFCContextBuilder:
|
||||
logger.error(f"获取聊天历史失败: {e}")
|
||||
return ""
|
||||
|
||||
async def _build_relation_info(self, sender_name: str, target_message: str) -> str:
|
||||
async def _build_relation_info(self, sender_name: str, target_message: str, user_id: Optional[str] = None) -> str:
|
||||
"""构建关系信息块"""
|
||||
config = _get_config()
|
||||
|
||||
@@ -135,11 +137,20 @@ class KFCContextBuilder:
|
||||
return "你将要回复的是你自己发送的消息。"
|
||||
|
||||
person_info_manager = get_person_info_manager()
|
||||
person_id = await person_info_manager.get_person_id_by_person_name(sender_name)
|
||||
|
||||
# 优先使用 user_id + platform 获取 person_id
|
||||
person_id = None
|
||||
if user_id and self.platform:
|
||||
person_id = person_info_manager.get_person_id(self.platform, user_id)
|
||||
logger.debug(f"通过 platform={self.platform}, user_id={user_id} 获取 person_id={person_id}")
|
||||
|
||||
# 如果没有找到,尝试通过 person_name 查找
|
||||
if not person_id:
|
||||
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},这是你们的第一次互动。"
|
||||
logger.debug(f"未找到用户 {sender_name} 的 person_id")
|
||||
return f"你与{sender_name}还没有建立深厚的关系,这是早期的互动阶段。"
|
||||
|
||||
try:
|
||||
from src.person_info.relationship_fetcher import relationship_fetcher_manager
|
||||
@@ -324,12 +335,13 @@ async def build_kfc_context(
|
||||
sender_name: str,
|
||||
target_message: str,
|
||||
context: Optional["StreamContext"] = None,
|
||||
user_id: Optional[str] = None,
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
便捷函数:构建KFC所需的所有上下文
|
||||
"""
|
||||
builder = KFCContextBuilder(chat_stream)
|
||||
return await builder.build_all_context(sender_name, target_message, context)
|
||||
return await builder.build_all_context(sender_name, target_message, context, user_id)
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -230,7 +230,7 @@ class ActionModel:
|
||||
|
||||
def get_description(self) -> str:
|
||||
"""获取动作的文字描述"""
|
||||
if self.type == "reply":
|
||||
if self.type == "kfc_reply":
|
||||
content = self.params.get("content", "")
|
||||
return f'发送消息:"{content[:50]}{"..." if len(content) > 50 else ""}"'
|
||||
elif self.type == "poke_user":
|
||||
@@ -305,12 +305,12 @@ class LLMResponse:
|
||||
|
||||
def has_reply(self) -> bool:
|
||||
"""是否包含回复动作"""
|
||||
return any(a.type in ("reply", "respond") for a in self.actions)
|
||||
return any(a.type in ("kfc_reply", "respond") for a in self.actions)
|
||||
|
||||
def get_reply_content(self) -> str:
|
||||
"""获取回复内容"""
|
||||
for action in self.actions:
|
||||
if action.type in ("reply", "respond"):
|
||||
if action.type in ("kfc_reply", "respond"):
|
||||
return action.params.get("content", "")
|
||||
return ""
|
||||
|
||||
|
||||
@@ -163,6 +163,16 @@ class ProactiveThinker:
|
||||
if not session.waiting_config.is_active():
|
||||
return
|
||||
|
||||
# 防止与 Chatter 并发处理:如果 Session 刚刚被更新(5秒内),跳过
|
||||
# 这样可以避免 Chatter 正在处理时,ProactiveThinker 也开始处理
|
||||
time_since_last_activity = time.time() - session.last_activity_at
|
||||
if time_since_last_activity < 5:
|
||||
logger.debug(
|
||||
f"[ProactiveThinker] Session {session.user_id} 刚有活动 "
|
||||
f"({time_since_last_activity:.1f}s ago),跳过处理"
|
||||
)
|
||||
return
|
||||
|
||||
# 检查是否超时
|
||||
if session.waiting_config.is_timeout():
|
||||
await self._handle_timeout(session)
|
||||
@@ -250,6 +260,19 @@ class ProactiveThinker:
|
||||
"""处理等待超时"""
|
||||
self._stats["timeout_decisions"] += 1
|
||||
|
||||
# 再次检查 Session 状态,防止在等待过程中被 Chatter 处理
|
||||
if session.status != SessionStatus.WAITING:
|
||||
logger.debug(f"[ProactiveThinker] Session {session.user_id} 已不在等待状态,跳过超时处理")
|
||||
return
|
||||
|
||||
# 再次检查最近活动时间
|
||||
time_since_last_activity = time.time() - session.last_activity_at
|
||||
if time_since_last_activity < 5:
|
||||
logger.debug(
|
||||
f"[ProactiveThinker] Session {session.user_id} 刚有活动,跳过超时处理"
|
||||
)
|
||||
return
|
||||
|
||||
logger.info(f"[ProactiveThinker] 等待超时: user={session.user_id}")
|
||||
|
||||
try:
|
||||
@@ -391,6 +414,14 @@ class ProactiveThinker:
|
||||
"""处理主动思考"""
|
||||
self._stats["proactive_triggered"] += 1
|
||||
|
||||
# 再次检查最近活动时间,防止与 Chatter 并发
|
||||
time_since_last_activity = time.time() - session.last_activity_at
|
||||
if time_since_last_activity < 5:
|
||||
logger.debug(
|
||||
f"[ProactiveThinker] Session {session.user_id} 刚有活动,跳过主动思考"
|
||||
)
|
||||
return
|
||||
|
||||
logger.info(f"[ProactiveThinker] 主动思考触发: user={session.user_id}, reason={trigger_reason}")
|
||||
|
||||
try:
|
||||
|
||||
@@ -63,11 +63,14 @@ class PromptBuilder:
|
||||
"""
|
||||
extra_context = extra_context or {}
|
||||
|
||||
# 获取 user_id(从 session 中)
|
||||
user_id = session.user_id if session else None
|
||||
|
||||
# 1. 构建人设块
|
||||
persona_block = self._build_persona_block()
|
||||
|
||||
# 2. 构建关系块
|
||||
relation_block = await self._build_relation_block(user_name, chat_stream)
|
||||
relation_block = await self._build_relation_block(user_name, chat_stream, user_id)
|
||||
|
||||
# 3. 构建活动流
|
||||
activity_stream = await self._build_activity_stream(session, user_name)
|
||||
@@ -123,6 +126,7 @@ class PromptBuilder:
|
||||
self,
|
||||
user_name: str,
|
||||
chat_stream: Optional["ChatStream"],
|
||||
user_id: Optional[str] = None,
|
||||
) -> str:
|
||||
"""构建关系块"""
|
||||
if not chat_stream:
|
||||
@@ -139,6 +143,7 @@ class PromptBuilder:
|
||||
sender_name=user_name,
|
||||
target_message="",
|
||||
context=None,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
relation_info = context_data.get("relation_info", "")
|
||||
@@ -253,7 +258,7 @@ class PromptBuilder:
|
||||
for action in actions:
|
||||
action_type = action.get("type", "unknown")
|
||||
|
||||
if action_type == "reply":
|
||||
if action_type == "kfc_reply":
|
||||
content = action.get("content", "")
|
||||
if len(content) > 50:
|
||||
content = content[:50] + "..."
|
||||
@@ -341,22 +346,111 @@ class PromptBuilder:
|
||||
)
|
||||
|
||||
def _build_actions_block(self, available_actions: Optional[dict]) -> str:
|
||||
"""构建可用动作块"""
|
||||
"""
|
||||
构建可用动作块
|
||||
|
||||
参考 AFC planner 的格式,为每个动作展示:
|
||||
- 动作名和描述
|
||||
- 使用场景
|
||||
- JSON 示例(含参数)
|
||||
"""
|
||||
if not available_actions:
|
||||
return self._get_default_actions_block()
|
||||
|
||||
lines = []
|
||||
for name, info in available_actions.items():
|
||||
desc = getattr(info, "description", "") or f"执行 {name}"
|
||||
lines.append(f"- `{name}`: {desc}")
|
||||
action_blocks = []
|
||||
for action_name, action_info in available_actions.items():
|
||||
block = self._format_single_action(action_name, action_info)
|
||||
if block:
|
||||
action_blocks.append(block)
|
||||
|
||||
return "\n".join(lines) if lines else self._get_default_actions_block()
|
||||
return "\n".join(action_blocks) if action_blocks else self._get_default_actions_block()
|
||||
|
||||
def _format_single_action(self, action_name: str, action_info) -> str:
|
||||
"""
|
||||
格式化单个动作为详细说明块
|
||||
|
||||
Args:
|
||||
action_name: 动作名称
|
||||
action_info: ActionInfo 对象
|
||||
|
||||
Returns:
|
||||
格式化后的动作说明
|
||||
"""
|
||||
# 获取动作描述
|
||||
description = getattr(action_info, "description", "") or f"执行 {action_name}"
|
||||
|
||||
# 获取使用场景
|
||||
action_require = getattr(action_info, "action_require", []) or []
|
||||
require_text = "\n".join(f" - {req}" for req in action_require) if action_require else " - 根据情况使用"
|
||||
|
||||
# 获取参数定义
|
||||
action_parameters = getattr(action_info, "action_parameters", {}) or {}
|
||||
|
||||
# 构建 action_data JSON 示例
|
||||
if action_parameters:
|
||||
param_lines = []
|
||||
for param_name, param_desc in action_parameters.items():
|
||||
param_lines.append(f' "{param_name}": "<{param_desc}>"')
|
||||
action_data_json = "{\n" + ",\n".join(param_lines) + "\n }"
|
||||
else:
|
||||
action_data_json = "{}"
|
||||
|
||||
# 构建完整的动作块
|
||||
return f"""### {action_name}
|
||||
**描述**: {description}
|
||||
|
||||
**使用场景**:
|
||||
{require_text}
|
||||
|
||||
**示例**:
|
||||
```json
|
||||
{{
|
||||
"type": "{action_name}",
|
||||
{f'"content": "<你要说的内容>"' if action_name == "kfc_reply" else self._build_params_example(action_parameters)}
|
||||
}}
|
||||
```
|
||||
"""
|
||||
|
||||
def _build_params_example(self, action_parameters: dict) -> str:
|
||||
"""构建参数示例字符串"""
|
||||
if not action_parameters:
|
||||
return '"_comment": "此动作无需额外参数"'
|
||||
|
||||
parts = []
|
||||
for param_name, param_desc in action_parameters.items():
|
||||
parts.append(f'"{param_name}": "<{param_desc}>"')
|
||||
|
||||
return ",\n ".join(parts)
|
||||
|
||||
def _get_default_actions_block(self) -> str:
|
||||
"""获取默认的动作列表"""
|
||||
return """- `reply`: 发送文字消息(参数:content)
|
||||
- `poke_user`: 戳一戳对方
|
||||
- `do_nothing`: 什么都不做"""
|
||||
return """### kfc_reply
|
||||
**描述**: 发送回复消息
|
||||
|
||||
**使用场景**:
|
||||
- 需要回复对方消息时使用
|
||||
|
||||
**示例**:
|
||||
```json
|
||||
{
|
||||
"type": "kfc_reply",
|
||||
"content": "你要说的话"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### do_nothing
|
||||
**描述**: 什么都不做
|
||||
|
||||
**使用场景**:
|
||||
- 当前不需要回应时使用
|
||||
|
||||
**示例**:
|
||||
```json
|
||||
{
|
||||
"type": "do_nothing"
|
||||
}
|
||||
```"""
|
||||
|
||||
async def _get_output_format(self) -> str:
|
||||
"""获取输出格式模板"""
|
||||
@@ -370,7 +464,7 @@ class PromptBuilder:
|
||||
return """请用 JSON 格式回复:
|
||||
{
|
||||
"thought": "你的想法",
|
||||
"actions": [{"type": "reply", "content": "你的回复"}],
|
||||
"actions": [{"type": "kfc_reply", "content": "你的回复"}],
|
||||
"expected_reaction": "期待的反应",
|
||||
"max_wait_seconds": 300
|
||||
}"""
|
||||
|
||||
@@ -47,20 +47,23 @@ KFC_V2_OUTPUT_FORMAT = Prompt(
|
||||
{{
|
||||
"thought": "你脑子里在想什么,越自然越好",
|
||||
"actions": [
|
||||
{{"type": "reply", "content": "你要说的话"}},
|
||||
{{"type": "其他动作", "参数": "值"}}
|
||||
{{"type": "动作名称", ...动作参数}}
|
||||
],
|
||||
"expected_reaction": "你期待对方的反应是什么",
|
||||
"max_wait_seconds": 300
|
||||
}}
|
||||
```
|
||||
|
||||
说明:
|
||||
- `thought`:你的内心独白,记录你此刻的想法和感受
|
||||
- `actions`:你要执行的动作列表,可以组合多个
|
||||
### 字段说明
|
||||
- `thought`:你的内心独白,记录你此刻的想法和感受。要自然,不要技术性语言。
|
||||
- `actions`:你要执行的动作列表。每个动作是一个对象,必须包含 `type` 字段指定动作类型,其他字段根据动作类型不同而不同(参考上面每个动作的示例)。
|
||||
- `expected_reaction`:你期待对方如何回应(用于判断是否需要等待)
|
||||
- `max_wait_seconds`:设定等待时间(秒),0 表示不等待,超时后你会考虑是否要主动说点什么
|
||||
- 即使什么都不想做,也放一个 `{{"type": "do_nothing"}}`""",
|
||||
|
||||
### 注意事项
|
||||
- 动作参数直接写在动作对象里,不需要 `action_data` 包装
|
||||
- 即使什么都不想做,也放一个 `{{"type": "do_nothing"}}`
|
||||
- 可以组合多个动作,比如先发消息再发表情""",
|
||||
)
|
||||
|
||||
# =================================================================================================
|
||||
|
||||
@@ -54,7 +54,9 @@ async def generate_response(
|
||||
extra_context=extra_context,
|
||||
)
|
||||
|
||||
logger.debug(f"[KFC Replyer] 构建的提示词:\n{prompt}")
|
||||
from src.config.config import global_config
|
||||
if global_config and global_config.debug.show_prompt:
|
||||
logger.info(f"[KFC Replyer] 生成的提示词:\n{prompt}")
|
||||
|
||||
# 2. 获取模型配置并调用 LLM
|
||||
models = llm_api.get_available_models()
|
||||
|
||||
@@ -197,7 +197,7 @@ class KokoroSession:
|
||||
for entry in reversed(self.mental_log):
|
||||
if entry.event_type == EventType.BOT_PLANNING:
|
||||
for action in entry.actions:
|
||||
if action.get("type") in ("reply", "respond"):
|
||||
if action.get("type") in ("kfc_reply", "respond"):
|
||||
return action.get("content", "")
|
||||
return None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user