From c4583e61d1350c6cfe77fd6c33031e8eab709135 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Sat, 29 Nov 2025 14:35:01 +0800 Subject: [PATCH] =?UTF-8?q?refactor(KFC):=20=E6=A8=A1=E5=9D=97=E5=8C=96?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E7=94=9F=E6=88=90=E5=B9=B6=E7=AE=80=E5=8C=96?= =?UTF-8?q?=E6=83=85=E7=BB=AA=E7=8A=B6=E6=80=81=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 此提交对 Kokoro Flow Chatter (KFC) 插件进行了重大重构,以提高模块化、可维护性和可靠性。 主要更改包括: - **提示生成**:原本的单一 `generate_system_prompt` 方法现在被委托给新的 `prompt_modules.py`。这实现了关注点分离,使管理提示的不同部分(如个性、上下文和动作定义)更加容易。 - **情绪状态处理**:已移除 `_update_emotional_state_from_thought` 中复杂且不可靠的基于关键词的情感分析。系统现在依赖 LLM 的显式 `update_internal_state` 动作,以更直接和准确地更新情绪状态。该方法已简化,仅处理轻微的参与度调整。 - **JSON 解析**:用统一的 `extract_and_parse_json` 工具替换了自定义 JSON 提取逻辑。这提供了更强大的解析能力,处理更大的Markdown 代码块和自动修复格式错误的 JSON。 - **调度器抽象**:引入了 `KFCSchedulerAdapter`,将聊天组件与全局调度器实现解耦,提高了可测试性和清晰度。 - **优雅的对话结束**:系统现在可以正确处理 `max_wait_seconds: 0`,立即结束对话主题并将会话设置为空闲状态,避免不必要的等待时间。 --- .../kokoro_flow_chatter/action_executor.py | 188 ++------ .../built_in/kokoro_flow_chatter/chatter.py | 35 +- .../kfc_scheduler_adapter.py | 394 +++++++++++++++++ .../kokoro_flow_chatter/prompt_generator.py | 77 +--- .../kokoro_flow_chatter/prompt_modules.py | 414 ++++++++++++++++++ 5 files changed, 870 insertions(+), 238 deletions(-) create mode 100644 src/plugins/built_in/kokoro_flow_chatter/kfc_scheduler_adapter.py create mode 100644 src/plugins/built_in/kokoro_flow_chatter/prompt_modules.py diff --git a/src/plugins/built_in/kokoro_flow_chatter/action_executor.py b/src/plugins/built_in/kokoro_flow_chatter/action_executor.py index 69f250ae6..78e9008d5 100644 --- a/src/plugins/built_in/kokoro_flow_chatter/action_executor.py +++ b/src/plugins/built_in/kokoro_flow_chatter/action_executor.py @@ -15,14 +15,15 @@ V5升级要点: """ import asyncio -import json -import re 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, @@ -38,33 +39,6 @@ if TYPE_CHECKING: logger = get_logger("kokoro_action_executor") -# ========== 情感分析关键词映射 ========== -# 正面情感关键词(提升mood_intensity和engagement_level) -POSITIVE_KEYWORDS = [ - "开心", "高兴", "快乐", "喜欢", "爱", "好奇", "期待", "惊喜", - "感动", "温暖", "舒服", "放松", "满足", "欣慰", "感激", "兴奋", - "有趣", "好玩", "可爱", "太棒了", "哈哈", "嘻嘻", "hiahia", -] - -# 负面情感关键词(降低mood_intensity,可能提升anxiety_level) -NEGATIVE_KEYWORDS = [ - "难过", "伤心", "失望", "沮丧", "担心", "焦虑", "害怕", "紧张", - "生气", "烦躁", "无聊", "疲惫", "累", "困", "不开心", "不高兴", - "委屈", "尴尬", "迷茫", "困惑", "郁闷", -] - -# 亲密关键词(提升relationship_warmth) -INTIMATE_KEYWORDS = [ - "喜欢你", "想你", "想念", "在乎", "关心", "信任", "依赖", - "亲爱的", "宝贝", "朋友", "陪伴", "一起", "我们", -] - -# 疏远关键词(降低relationship_warmth) -DISTANT_KEYWORDS = [ - "讨厌", "烦人", "无聊", "不想理", "走开", "别烦", - "算了", "随便", "不在乎", -] - class ActionExecutor: """ @@ -146,75 +120,25 @@ class ActionExecutor: """ 解析LLM的JSON响应 + 使用统一的json_parser工具进行解析,自动处理: + - Markdown代码块标记 + - 格式错误的JSON修复(json_repair) + - 多种包装格式 + Args: response_text: LLM返回的原始文本 Returns: LLMResponseModel: 解析后的响应模型 - - Raises: - ValueError: 如果解析失败 """ - # 尝试提取JSON块 - json_str = self._extract_json(response_text) + # 使用统一的json_parser工具解析 + data = extract_and_parse_json(response_text, strict=False) - if not json_str: - logger.warning(f"无法从LLM响应中提取JSON: {response_text[:200]}...") + if not data or not isinstance(data, dict): + logger.warning(f"无法从LLM响应中提取有效JSON: {response_text[:200]}...") return LLMResponseModel.create_error_response("无法解析响应格式") - try: - data = json.loads(json_str) - return self._validate_and_create_response(data) - except json.JSONDecodeError as e: - logger.error(f"JSON解析失败: {e}, 原始文本: {json_str[:200]}...") - return LLMResponseModel.create_error_response(f"JSON解析错误: {e}") - - def _extract_json(self, text: str) -> Optional[str]: - """ - 从文本中提取JSON块 - - 支持以下格式: - 1. 纯JSON字符串 - 2. ```json ... ``` 代码块 - 3. 文本中嵌入的JSON对象 - """ - text = text.strip() - - # 尝试1:直接解析(如果整个文本就是JSON) - if text.startswith("{"): - # 找到匹配的结束括号 - brace_count = 0 - for i, char in enumerate(text): - if char == "{": - brace_count += 1 - elif char == "}": - brace_count -= 1 - if brace_count == 0: - return text[:i + 1] - - # 尝试2:提取 ```json ... ``` 代码块 - json_block_pattern = r"```(?:json)?\s*([\s\S]*?)```" - matches = re.findall(json_block_pattern, text) - for match in matches: - match = match.strip() - if match.startswith("{"): - try: - json.loads(match) - return match - except json.JSONDecodeError: - continue - - # 尝试3:寻找文本中的JSON对象 - json_pattern = r"\{[\s\S]*\}" - matches = re.findall(json_pattern, text) - for match in matches: - try: - json.loads(match) - return match - except json.JSONDecodeError: - continue - - return None + return self._validate_and_create_response(data) def _validate_and_create_response(self, data: dict[str, Any]) -> LLMResponseModel: """ @@ -239,10 +163,11 @@ class ActionExecutor: 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(60, min(wait_seconds, 600)) + data["max_wait_seconds"] = max(0, min(wait_seconds, 900)) except (ValueError, TypeError): data["max_wait_seconds"] = 300 @@ -706,12 +631,13 @@ class ActionExecutor: session: KokoroSession, ) -> None: """ - V5:根据thought字段的情感倾向动态更新EmotionalState + 根据thought字段更新EmotionalState - 分析策略: - 1. 统计正面/负面/亲密/疏远关键词出现次数 - 2. 根据关键词比例计算情感调整值 - 3. 应用平滑的情感微调(每次变化不超过±0.1) + V6重构: + - 移除基于关键词的情感分析(诡异且不准确) + - 情感状态现在主要通过LLM输出的update_internal_state动作更新 + - 关系温度应该从person_info/relationship_manager的好感度系统读取 + - 此方法仅做简单的engagement_level更新 Args: thought: LLM返回的内心独白 @@ -720,76 +646,16 @@ class ActionExecutor: if not thought: return - thought_lower = thought.lower() emotional_state = session.emotional_state - # 统计关键词 - positive_count = sum(1 for kw in POSITIVE_KEYWORDS if kw in thought_lower) - negative_count = sum(1 for kw in NEGATIVE_KEYWORDS if kw in thought_lower) - intimate_count = sum(1 for kw in INTIMATE_KEYWORDS if kw in thought_lower) - distant_count = sum(1 for kw in DISTANT_KEYWORDS if kw in thought_lower) - - # 计算情感倾向分数 (-1.0 到 1.0) - total_mood = positive_count + negative_count - if total_mood > 0: - mood_score = (positive_count - negative_count) / total_mood - else: - mood_score = 0.0 - - total_relation = intimate_count + distant_count - if total_relation > 0: - relation_score = (intimate_count - distant_count) / total_relation - else: - relation_score = 0.0 - - # 应用情感微调(每次最多变化±0.05) - adjustment_rate = 0.05 - - # 更新 mood_intensity - if mood_score != 0: - old_intensity = emotional_state.mood_intensity - new_intensity = old_intensity + (mood_score * adjustment_rate) - emotional_state.mood_intensity = max(0.0, min(1.0, new_intensity)) - - # 更新 mood 文字描述 - if mood_score > 0.3: - emotional_state.mood = "开心" - elif mood_score < -0.3: - emotional_state.mood = "低落" - # 小于0.3的变化不改变mood文字 - - # 更新 anxiety_level(负面情绪增加焦虑) - if negative_count > 0 and negative_count > positive_count: - old_anxiety = emotional_state.anxiety_level - new_anxiety = old_anxiety + (adjustment_rate * 0.5) - emotional_state.anxiety_level = max(0.0, min(1.0, new_anxiety)) - elif positive_count > negative_count * 2: - # 非常积极时降低焦虑 - old_anxiety = emotional_state.anxiety_level - new_anxiety = old_anxiety - (adjustment_rate * 0.3) - emotional_state.anxiety_level = max(0.0, min(1.0, new_anxiety)) - - # 更新 relationship_warmth - if relation_score != 0: - old_warmth = emotional_state.relationship_warmth - new_warmth = old_warmth + (relation_score * adjustment_rate) - emotional_state.relationship_warmth = max(0.0, min(1.0, new_warmth)) - - # 更新 engagement_level(有内容的thought表示高投入) + # 简单的engagement_level更新:有内容的thought表示高投入 if len(thought) > 50: old_engagement = emotional_state.engagement_level - new_engagement = old_engagement + (adjustment_rate * 0.5) + 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() - # 记录情感变化日志 - if positive_count + negative_count + intimate_count + distant_count > 0: - logger.info( - f"[KFC] 动态情感更新: " - f"正面={positive_count}, 负面={negative_count}, " - f"亲密={intimate_count}, 疏远={distant_count} | " - f"mood_intensity={emotional_state.mood_intensity:.2f}, " - f"relationship_warmth={emotional_state.relationship_warmth:.2f}, " - f"anxiety_level={emotional_state.anxiety_level:.2f}" - ) + # 注意:关系温度(relationship_warmth)应该从全局的好感度系统读取 + # 参考 src/person_info/relationship_manager.py 和 src/plugin_system/apis/person_api.py + # 当前实现中,这个值主要通过 LLM 的 update_internal_state 动作来更新 diff --git a/src/plugins/built_in/kokoro_flow_chatter/chatter.py b/src/plugins/built_in/kokoro_flow_chatter/chatter.py index 4b231550d..1daec1023 100644 --- a/src/plugins/built_in/kokoro_flow_chatter/chatter.py +++ b/src/plugins/built_in/kokoro_flow_chatter/chatter.py @@ -29,7 +29,7 @@ from .models import ( SessionStatus, ) from .prompt_generator import PromptGenerator, get_prompt_generator -from .scheduler import BackgroundScheduler, get_scheduler +from .kfc_scheduler_adapter import KFCSchedulerAdapter, get_scheduler from .session_manager import SessionManager, get_session_manager if TYPE_CHECKING: @@ -79,7 +79,7 @@ class KokoroFlowChatter(BaseChatter): # 核心组件 self.session_manager: SessionManager = get_session_manager() self.prompt_generator: PromptGenerator = get_prompt_generator() - self.scheduler: BackgroundScheduler = get_scheduler() + self.scheduler: KFCSchedulerAdapter = get_scheduler() self.action_executor: ActionExecutor = ActionExecutor(stream_id) # 配置 @@ -296,17 +296,30 @@ class KokoroFlowChatter(BaseChatter): # 10. 处理执行结果 if execution_result["has_reply"]: - # 如果发送了回复,进入等待状态 - session.start_waiting( - expected_reaction=parsed_response.expected_user_reaction, - max_wait=parsed_response.max_wait_seconds - ) + # 如果发送了回复,检查是否需要进入等待状态 + 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 - logger.debug( - f"[KFC] 进入等待状态: user={user_id}, " - f"max_wait={parsed_response.max_wait_seconds}s" - ) else: # 没有发送回复,返回空闲状态 session.status = SessionStatus.IDLE diff --git a/src/plugins/built_in/kokoro_flow_chatter/kfc_scheduler_adapter.py b/src/plugins/built_in/kokoro_flow_chatter/kfc_scheduler_adapter.py new file mode 100644 index 000000000..a18d50b7a --- /dev/null +++ b/src/plugins/built_in/kokoro_flow_chatter/kfc_scheduler_adapter.py @@ -0,0 +1,394 @@ +""" +Kokoro Flow Chatter 调度器适配器 + +基于项目统一的 UnifiedScheduler 实现 KFC 的定时任务功能。 +不再自己创建后台循环,而是复用全局调度器的基础设施。 + +核心功能: +1. 会话等待超时检测 +2. 连续思考触发 +3. 与 UnifiedScheduler 的集成 +""" + +import time +from typing import TYPE_CHECKING, Any, Callable, Coroutine, Optional + +from src.common.logger import get_logger +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. 处理等待超时并触发决策 + """ + + # 连续思考触发点(等待进度的百分比) + CONTINUOUS_THINKING_TRIGGERS = [0.3, 0.6, 0.85] + + # 任务名称常量 + TASK_NAME_WAITING_CHECK = "kfc_waiting_check" + + 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, + ): + """ + 初始化调度器适配器 + + Args: + check_interval: 检查间隔(秒) + on_timeout_callback: 超时回调函数 + on_continuous_thinking_callback: 连续思考回调函数 + """ + self.check_interval = check_interval + self.on_timeout_callback = on_timeout_callback + self.on_continuous_thinking_callback = on_continuous_thinking_callback + + self._registered = False + self._schedule_id: Optional[str] = None + + # 统计信息 + self._stats = { + "total_checks": 0, + "timeouts_triggered": 0, + "continuous_thinking_triggered": 0, + "last_check_time": 0.0, + } + + logger.info("KFCSchedulerAdapter 初始化完成") + + async def start(self) -> None: + """启动调度器(注册到 UnifiedScheduler)""" + if self._registered: + logger.warning("KFC 调度器已在运行中") + return + + # 注册周期性检查任务 + 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, # 单次检查超时 30 秒 + ) + + 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 调度器已从 UnifiedScheduler 注销: schedule_id={self._schedule_id}") + except Exception as e: + logger.error(f"停止 KFC 调度器时出错: {e}") + finally: + self._registered = False + self._schedule_id = None + + async def _check_waiting_sessions(self) -> None: + """检查所有等待中的会话(由 UnifiedScheduler 调用)""" + 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 + + for session in waiting_sessions: + try: + await self._process_waiting_session(session) + 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) + + 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 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, +) -> KFCSchedulerAdapter: + """ + 初始化并启动调度器 + + Args: + check_interval: 检查间隔 + on_timeout_callback: 超时回调 + on_continuous_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, + ) + 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 diff --git a/src/plugins/built_in/kokoro_flow_chatter/prompt_generator.py b/src/plugins/built_in/kokoro_flow_chatter/prompt_generator.py index 0e793439e..4e8a18a2c 100644 --- a/src/plugins/built_in/kokoro_flow_chatter/prompt_generator.py +++ b/src/plugins/built_in/kokoro_flow_chatter/prompt_generator.py @@ -441,8 +441,8 @@ class PromptGenerator: for param_name, param_desc in action_info.action_parameters.items(): example_params[param_name] = f"<{param_desc}>" - import json - params_json = json.dumps(example_params, ensure_ascii=False, indent=2) if example_params else "{}" + import orjson + params_json = orjson.dumps(example_params, option=orjson.OPT_INDENT_2).decode('utf-8') if example_params else "{}" action_block += f""" **示例**: ```json @@ -508,8 +508,9 @@ class PromptGenerator: """ 生成系统提示词 - V4升级:从 global_config.personality 读取完整人设 - V5超融合:集成S4U所有上下文模块 + V6模块化升级:使用 prompt_modules 构建模块化的提示词 + - 每个模块独立构建,职责清晰 + - 回复相关(人设、上下文)与动作定义分离 Args: session: 当前会话 @@ -520,69 +521,13 @@ class PromptGenerator: Returns: str: 系统提示词 """ - from src.config.config import global_config - from datetime import datetime + from .prompt_modules import build_system_prompt - emotional_params = self._format_emotional_state(session.emotional_state) - - # 格式化可用动作 - available_actions_block = self._format_available_actions(available_actions or {}) - - # 从 global_config.personality 读取完整人设 - if global_config is None: - raise RuntimeError("global_config 未初始化") - - personality_cfg = global_config.personality - - # 核心人设 - personality_core = personality_cfg.personality_core or self.persona_description - personality_side = personality_cfg.personality_side or "" - identity = personality_cfg.identity or "" - background_story = personality_cfg.background_story or "" - reply_style = personality_cfg.reply_style or "" - - # 安全规则:转换为格式化字符串 - safety_guidelines = personality_cfg.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) - - # 构建当前时间 - current_time = datetime.now().strftime("%Y年%m月%d日 %H:%M:%S") - - # 判断聊天场景 - is_group_chat = False - if chat_stream: - is_group_chat = bool(chat_stream.group_info) - chat_scene = "群聊" if is_group_chat else "私聊" - - # 从context_data提取S4U上下文模块(如果提供) - context_data = context_data or {} - relation_info_block = context_data.get("relation_info", "") - memory_block = context_data.get("memory_block", "") - expression_habits_block = context_data.get("expression_habits", "") - schedule_block = context_data.get("schedule", "") - - # 如果有日程,添加前缀 - if schedule_block: - schedule_block = f"**当前活动**: {schedule_block}" - - return self.SYSTEM_PROMPT_TEMPLATE.format( - personality_core=personality_core, - personality_side=personality_side, - identity=identity, - background_story=background_story, - reply_style=reply_style, - safety_guidelines=safety_guidelines_str, - available_actions_block=available_actions_block, - current_time=current_time, - chat_scene=chat_scene, - relation_info_block=relation_info_block or "(暂无关系信息)", - memory_block=memory_block or "", - expression_habits_block=expression_habits_block or "", - schedule_block=schedule_block, - **emotional_params, + return build_system_prompt( + session=session, + available_actions=available_actions, + context_data=context_data, + chat_stream=chat_stream, ) def generate_responding_prompt( diff --git a/src/plugins/built_in/kokoro_flow_chatter/prompt_modules.py b/src/plugins/built_in/kokoro_flow_chatter/prompt_modules.py new file mode 100644 index 000000000..1cd4edad2 --- /dev/null +++ b/src/plugins/built_in/kokoro_flow_chatter/prompt_modules.py @@ -0,0 +1,414 @@ +""" +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. 核心身份与人格 (Core Identity & Personality)", + personality_core, + ] + + if personality_side: + parts.append(f"\n### 人格细节\n{personality_side}") + + if identity: + parts.append(f"\n### 外貌与身份特征\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 = """### KFC核心准则(极致拟人) +1. **你是真实存在的人**:你不是AI、不是助手、不是工具。你是一个有血有肉、有自己生活和想法的人 +2. **感受优先**:在做出任何回应前,先以"人"的方式体验和感受当前的情境 +3. **真实的内心活动**:保持真实的内心独白,可以有情绪、猜测、犹豫、甚至不想搭理人的时候 +4. **自然的社交距离**:像真人一样有自己的边界感,不会无条件迎合,也不会刻意疏远 +5. **有自己的生活**:你有自己的事情要做、自己的兴趣爱好、自己的情绪起伏,不是随叫随到的存在""" + + return f"""## 2. 核心行为准则 (Core Interaction Rules) +{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", "") + if schedule_block: + schedule_block = f"\n**当前活动**: {schedule_block}" + + # 内在状态 + es = session.emotional_state + inner_state = f"""### 你的内在状态 +当前心情:{es.mood}(强度:{es.mood_intensity:.1%}) +与用户的关系热度:{es.relationship_warmth:.1%} +对用户的印象:{es.impression_of_user or "还没有形成明确的印象"} +当前焦虑程度:{es.anxiety_level:.1%} +投入程度:{es.engagement_level:.1%}""" + + # 关系信息 + relation_info = context_data.get("relation_info", "") + relation_block = relation_info if relation_info else "(暂无关系信息)" + + # 记忆 + memory_block = context_data.get("memory_block", "") + + parts = [ + "## 3. 当前情境 (Current Context)", + f"**时间**: {current_time}", + f"**场景**: {chat_scene}", + ] + + if schedule_block: + parts.append(schedule_block) + + parts.append("") + parts.append(inner_state) + parts.append("") + parts.append("## 4. 关系网络与记忆 (Relationships & Memories)") + parts.append(relation_block) + + 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. 你的可用能力 (Available Actions) +你可以根据内心想法,自由选择并组合以下行动来回应用户: + +{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} 动作" + + # 参数说明 + 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}>" + + 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() -> 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) + - `relationship_warmth`: 关系热度(0.0-1.0) + - `impression_of_user`: 对用户的印象描述 + - `anxiety_level`: 焦虑程度(0.0-1.0) + - `engagement_level`: 投入程度(0.0-1.0) +**示例**: +```json +{"type": "update_internal_state", "mood": "开心", "mood_intensity": 0.8} +``` + +### `do_nothing` +**描述**: 明确表示"思考后决定不作回应" +**参数**: 无 +**示例**: +```json +{"type": "do_nothing", "reason": "现在不是说话的好时机"} +```""" + + +# ============================================================ +# 模块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 输出格式说明 - 强调 max_wait_seconds 可以为 0 + json_format = """### JSON输出格式要求 +你必须以JSON格式输出,包含以下字段: + +```json +{ + "thought": "(必须)你当下的内心独白,自由流动的想法,包含你选择这些行动的"情感盘算"", + "expected_user_reaction": "(必须)你预期用户会如何回应", + "max_wait_seconds": (必须)你愿意等待的最长秒数(整数,0-900之间,0表示不等待), + "actions": [ + { + "type": "action_type", + "reason": "选择这个行动的原因(可选但推荐)", + ...其他参数 + } + ] +} +``` + +### 重要说明: +- `thought` 是你的"心",应该是自然的内心想法,包含情绪和不确定性 +- `actions` 是你的"手",是一个**可编排的剧本**,可以组合多个行动形成完整的互动体验 +- 你可以根据情况组合多个动作,比如:先唱歌舒缓情绪,再发图片逗笑,最后用语音表达关心 +- `max_wait_seconds` 应该根据对话的重要性和氛围动态调整(上限900秒) + - **重要**: 当话题已经自然结束、用户说"拜拜/晚安/再见"、或者你认为不需要继续等待用户回复时,设为 **0** +- 即使决定不做任何事,也要有 `{"type": "do_nothing"}` 动作""" + + parts = ["## 6. 表达方式与输出格式 (Expression Style & Output Format)"] + + 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 = '你是一个有主观体验的对话伙伴,而不是一个问答工具。你需要"感受"、"思考"并"体验"这段对话。' + + # 组装各模块 + 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)