refactor(KFC): 模块化提示生成并简化情绪状态处理

此提交对 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`,立即结束对话主题并将会话设置为空闲状态,避免不必要的等待时间。
This commit is contained in:
tt-P607
2025-11-29 14:35:01 +08:00
parent 703b5724f9
commit c4583e61d1
5 changed files with 870 additions and 238 deletions

View File

@@ -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
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 动作来更新

View File

@@ -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"]:
# 如果发送了回复,进入等待状态
# 如果发送了回复,检查是否需要进入等待状态
max_wait = parsed_response.max_wait_seconds
if max_wait > 0:
# 正常等待状态
session.start_waiting(
expected_reaction=parsed_response.expected_user_reaction,
max_wait=parsed_response.max_wait_seconds
max_wait=max_wait
)
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"
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

View File

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

View File

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

View File

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