feat(KFC): 🎉 Kokoro Flow Chatter 心流聊天器 - 私聊专属处理系统从零构建完成

这是一个全新的私聊聊天处理器,专为深度情感交互设计,从架构设计到代码实现全部从零完成。

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🏗️ 核心架构 (7个核心模块)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

📁 src/plugins/built_in/kokoro_flow_chatter/
├── chatter.py           # 主处理器 - 协调所有组件的核心类
├── context_builder.py   # S4U上下文构建器 - 超融合上下文系统
├── prompt_generator.py  # V6三明治提示词生成器
├── action_executor.py   # 动作执行器 - 解析+执行LLM动作
├── response_post_processor.py  # 回复后处理器 - 分割+错别字
├── models.py            # 数据模型 - Session/情感状态/心理日志
├── session_manager.py   # 会话管理器 - 用户状态持久化
├── scheduler.py         # 调度器 - 主动思考/超时处理
├── config.py            # 配置类
└── plugin.py            # 插件注册入口

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 核心特性
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

【V1-V3 基础框架】
- 心理状态驱动的交互模型 (KokoroSession)
- 连续时间观念和等待体验 (IDLE→RESPONDING→WAITING状态机)
- 心理日志系统 (MentalLogEntry)
- 动态情感状态 (EmotionalState)

【V4 动作系统集成】
- 动态动作发现 (复用ChatterActionManager)
- 支持所有AFC动作 (reply/emoji/poke_user/set_emoji_like等)
- LLM响应JSON解析和验证

【V5 超融合上下文】
- S4U用户中心上下文检索
- 三层记忆系统集成 (感知/短期/长期)
- 时间感知块 (时间段+日程+情境)
- 人物关系信息注入
- 跨聊天上下文共享

【V6 最终优化】
- 三明治提示词结构 (系统层→上下文层→指令层)
- ActionModifier动作筛选器集成 (三阶段预筛选)
  - 阶段0: 聊天类型过滤
  - 阶段2: 关联类型匹配
  - 阶段3: go_activate()激活判定
- 回复分割器复用AFC核心逻辑 (split_into_sentences_w_remove_punctuation)
- 修复model配置 (使用replyer而非utils)
- 修复context_builder异步问题

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔧 技术细节
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

提示词结构 (V6三明治):
┌─────────────────────────────────────┐
│ 🍞 系统层 (人设/身份/表达风格)        │
├─────────────────────────────────────┤
│ 🥬 上下文层                          │
│  ├─ 时间感知块                       │
│  ├─ 三层记忆 (感知+短期+长期)         │
│  ├─ 人物关系                         │
│  ├─ 对话历史                         │
│  └─ 用户最新消息                     │
├─────────────────────────────────────┤
│ 🍞 指令层 (JSON输出格式/可用动作)     │
└─────────────────────────────────────┘

动作筛选效果: 13个动作 → 约5-7个 (节省token+提升决策质量)
回复分割: 长消息自动按标点分割成多条发送

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📝 配置项 (bot_config.toml)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[kokoro_flow_chatter]
enable = true
max_wait_seconds_default = 300
enable_continuous_thinking = true

[kokoro_flow_chatter.proactive_thinking]
enabled = true
silence_threshold_seconds = 7200
min_affinity_for_proactive = 0.3
min_interval_between_proactive = 1800
enable_morning_greeting = true
enable_night_greeting = true

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🎯 设计理念
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

KFC不是独立人格,而是:
- 复用全局人设、情感框架和回复模型
- 专注于"体验→决策→行动"的私聊交互模式
- 从"消息响应者"转变为"对话体验者"
- 深度情感连接和长期关系维护
This commit is contained in:
tt-P607
2025-11-29 02:05:56 +08:00
parent d2d0cfc4db
commit 0746a73bce
16 changed files with 5415 additions and 25 deletions

View File

@@ -674,6 +674,14 @@ DEFAULT_MODULE_COLORS = {
"AioHTTP-Gemini客户端": "#5FD7FF",
"napcat_adapter": "#5F87AF", # 柔和的灰蓝色,不刺眼且低调
"event_manager": "#5FD7AF", # 柔和的蓝绿色,稍微醒目但不刺眼
# Kokoro Flow Chatter (KFC) 相关 - 超融合架构专用颜色
"kokoro_flow_chatter": "#FF5FAF", # 粉紫色 - 主聊天器
"kokoro_prompt_generator": "#00D7FF", # 青色 - Prompt构建
"kokoro_action_executor": "#FFFF00", # 黄色 - 动作解析与执行
"kfc_context_builder": "#5FD7FF", # 蓝色 - 上下文构建
"kfc_session_manager": "#87D787", # 绿色 - 会话管理
"kfc_scheduler": "#D787AF", # 柔和粉色 - 调度器
"kfc_post_processor": "#5F87FF", # 蓝色 - 后处理
}
DEFAULT_MODULE_ALIASES = {
@@ -802,6 +810,14 @@ DEFAULT_MODULE_ALIASES = {
"db_migration": "数据库迁移",
"小彩蛋": "小彩蛋",
"AioHTTP-Gemini客户端": "AioHTTP-Gemini客户端",
# Kokoro Flow Chatter (KFC) 超融合架构相关
"kokoro_flow_chatter": "心流聊天",
"kokoro_prompt_generator": "KFC提示词",
"kokoro_action_executor": "KFC动作",
"kfc_context_builder": "KFC上下文",
"kfc_session_manager": "KFC会话",
"kfc_scheduler": "KFC调度",
"kfc_post_processor": "KFC后处理",
}

View File

@@ -25,6 +25,7 @@ from src.config.official_configs import (
EmojiConfig,
ExperimentalConfig,
ExpressionConfig,
KokoroFlowChatterConfig,
LPMMKnowledgeConfig,
MessageBusConfig,
MemoryConfig,
@@ -425,6 +426,9 @@ class Config(ValidatedConfigBase):
proactive_thinking: ProactiveThinkingConfig = Field(
default_factory=lambda: ProactiveThinkingConfig(), description="主动思考配置"
)
kokoro_flow_chatter: KokoroFlowChatterConfig = Field(
default_factory=lambda: KokoroFlowChatterConfig(), description="心流对话系统配置(私聊专用)"
)
plugin_http_system: PluginHttpSystemConfig = Field(
default_factory=lambda: PluginHttpSystemConfig(), description="插件HTTP端点系统配置"
)

View File

@@ -911,3 +911,71 @@ class ProactiveThinkingConfig(ValidatedConfigBase):
# --- 新增:调试与监控 ---
enable_statistics: bool = Field(default=True, description="是否启用统计功能(记录触发次数、决策分布等)")
log_decisions: bool = Field(default=False, description="是否记录每次决策的详细日志(用于调试)")
class KokoroFlowChatterProactiveConfig(ValidatedConfigBase):
"""
Kokoro Flow Chatter 主动思考子配置
设计哲学:主动行为源于内部状态和外部环境的自然反应,而非机械的限制。
她的主动是因为挂念、因为关心、因为想问候,而不是因为"任务"
"""
enabled: bool = Field(default=True, description="是否启用KFC的私聊主动思考")
# 1. 沉默触发器:当感到长久的沉默时,她可能会想说些什么
silence_threshold_seconds: int = Field(
default=7200, ge=60, le=86400,
description="用户沉默超过此时长可能触发主动思考默认2小时"
)
# 2. 关系门槛:她不会对不熟悉的人过于主动
min_affinity_for_proactive: float = Field(
default=0.3, ge=0.0, le=1.0,
description="需要达到最低好感度,她才会开始主动关心"
)
# 3. 频率呼吸:为了避免打扰,她的关心总是有间隔的
min_interval_between_proactive: int = Field(
default=1800, ge=0,
description="两次主动思考之间的最小间隔默认30分钟"
)
# 4. 自然问候:在特定的时间,她会像朋友一样送上问候
enable_morning_greeting: bool = Field(
default=True, description="是否启用早安问候 (例如: 8:00 - 9:00)"
)
enable_night_greeting: bool = Field(
default=True, description="是否启用晚安问候 (例如: 22:00 - 23:00)"
)
class KokoroFlowChatterConfig(ValidatedConfigBase):
"""
Kokoro Flow Chatter 配置类 - 私聊专用心流对话系统
设计理念KFC不是独立人格它复用全局的人设、情感框架和回复模型
只作为Bot核心人格在私聊中的一种特殊表现模式。
"""
# --- 总开关 ---
enable: bool = Field(
default=True,
description="开启后KFC将接管所有私聊消息关闭后私聊消息将由AFC处理"
)
# --- 核心行为配置 ---
max_wait_seconds_default: int = Field(
default=300, ge=30, le=3600,
description="默认的最大等待秒数AI发送消息后愿意等待用户回复的时间"
)
enable_continuous_thinking: bool = Field(
default=True,
description="是否在等待期间启用心理活动更新"
)
# --- 私聊专属主动思考配置 ---
proactive_thinking: KokoroFlowChatterProactiveConfig = Field(
default_factory=KokoroFlowChatterProactiveConfig,
description="私聊专属主动思考配置"
)

View File

@@ -0,0 +1,26 @@
"""
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__"]

View File

@@ -0,0 +1,795 @@
"""
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 json
import re
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.base.component_types import ActionInfo
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")
# ========== 情感分析关键词映射 ==========
# 正面情感关键词提升mood_intensity和engagement_level
POSITIVE_KEYWORDS = [
"开心", "高兴", "快乐", "喜欢", "", "好奇", "期待", "惊喜",
"感动", "温暖", "舒服", "放松", "满足", "欣慰", "感激", "兴奋",
"有趣", "好玩", "可爱", "太棒了", "哈哈", "嘻嘻", "hiahia",
]
# 负面情感关键词降低mood_intensity可能提升anxiety_level
NEGATIVE_KEYWORDS = [
"难过", "伤心", "失望", "沮丧", "担心", "焦虑", "害怕", "紧张",
"生气", "烦躁", "无聊", "疲惫", "", "", "不开心", "不高兴",
"委屈", "尴尬", "迷茫", "困惑", "郁闷",
]
# 亲密关键词提升relationship_warmth
INTIMATE_KEYWORDS = [
"喜欢你", "想你", "想念", "在乎", "关心", "信任", "依赖",
"亲爱的", "宝贝", "朋友", "陪伴", "一起", "我们",
]
# 疏远关键词降低relationship_warmth
DISTANT_KEYWORDS = [
"讨厌", "烦人", "无聊", "不想理", "走开", "别烦",
"算了", "随便", "不在乎",
]
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响应
Args:
response_text: LLM返回的原始文本
Returns:
LLMResponseModel: 解析后的响应模型
Raises:
ValueError: 如果解析失败
"""
# 尝试提取JSON块
json_str = self._extract_json(response_text)
if not json_str:
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:
"""
验证并创建响应模型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:
# 确保在合理范围内
try:
wait_seconds = int(data["max_wait_seconds"])
data["max_wait_seconds"] = max(60, min(wait_seconds, 600))
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]:
"""
执行内部状态更新动作
这个动作用于实现情感闭环让AI可以主动更新自己的情感状态
"""
updated_fields = []
emotional_state = session.emotional_state
if "mood" in params:
emotional_state.mood = params["mood"]
updated_fields.append("mood")
if "mood_intensity" in params:
try:
intensity = float(params["mood_intensity"])
emotional_state.mood_intensity = max(0.0, min(1.0, intensity))
updated_fields.append("mood_intensity")
except (ValueError, TypeError):
pass
if "relationship_warmth" in params:
try:
warmth = float(params["relationship_warmth"])
emotional_state.relationship_warmth = max(0.0, min(1.0, warmth))
updated_fields.append("relationship_warmth")
except (ValueError, TypeError):
pass
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()
logger.debug(f"更新情感状态: {updated_fields}")
return {
"action_type": "update_internal_state",
"success": True,
"updated_fields": updated_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:
"""
V5根据thought字段的情感倾向动态更新EmotionalState
分析策略:
1. 统计正面/负面/亲密/疏远关键词出现次数
2. 根据关键词比例计算情感调整值
3. 应用平滑的情感微调每次变化不超过±0.1
Args:
thought: LLM返回的内心独白
session: 当前会话
"""
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表示高投入
if len(thought) > 50:
old_engagement = emotional_state.engagement_level
new_engagement = old_engagement + (adjustment_rate * 0.5)
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}"
)

View File

@@ -0,0 +1,606 @@
"""
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 .scheduler import BackgroundScheduler, 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: BackgroundScheduler = get_scheduler()
self.action_executor: ActionExecutor = ActionExecutor(stream_id)
# 配置
self._load_config()
# 并发控制
self._lock = asyncio.Lock()
# 统计信息
self.stats = {
"messages_processed": 0,
"llm_calls": 0,
"successful_responses": 0,
"failed_responses": 0,
"timeout_decisions": 0,
}
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
)
async def execute(self, context: StreamContext) -> dict:
"""
执行聊天处理逻辑BaseChatter接口实现
Args:
context: StreamContext对象包含聊天上下文信息
Returns:
处理结果字典
"""
async with self._lock:
try:
self.last_activity_time = time.time()
# 获取未读消息(提前获取用于动作筛选)
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")
# 处理最后一条消息
target_message = unread_messages[-1]
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} 个动作")
# 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)} 个)"
)
# 执行核心处理流程(传递筛选后的动作)
result = await self._handle_message(target_message, context, available_actions)
# 更新统计
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
)
async def _handle_message(
self,
message: "DatabaseMessages",
context: StreamContext,
available_actions: dict | None = None,
) -> dict:
"""
处理单条消息的核心逻辑
实现"体验 -> 决策 -> 行动"的交互模式
V5超融合集成S4U所有上下文模块
Args:
message: 要处理的消息
context: 聊天上下文
available_actions: 可用动作字典V2新增
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()
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上下文
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: 聊天流用于场景判断
)
# 7. 调用LLM
llm_response = await self._call_llm(system_prompt, user_prompt)
self.stats["llm_calls"] += 1
# 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"]:
# 如果发送了回复,进入等待状态
session.start_waiting(
expected_reaction=parsed_response.expected_user_reaction,
max_wait=parsed_response.max_wait_seconds
)
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
logger.debug(f"[KFC] 无回复动作,返回空闲: user={user_id}")
# 11. 保存会话
await self.session_manager.save_session(user_id)
# 12. 标记消息为已读
context.mark_message_as_read(str(message.message_id))
return self._build_result(
success=True,
message="processed",
has_reply=execution_result["has_reply"],
thought=parsed_response.thought,
)
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):
"""获取聊天流对象"""
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(self.stream_id)
except Exception as e:
logger.warning(f"[KFC] 获取chat_stream失败: {e}")
return None
async def _on_session_timeout(self, session: KokoroSession) -> None:
"""
会话超时回调
当等待超时时,触发后续决策流程
Args:
session: 超时的会话
"""
logger.info(f"[KFC] 处理超时决策: user={session.user_id}")
self.stats["timeout_decisions"] += 1
try:
# V2: 加载可用动作
available_actions = await self.action_executor.load_actions()
# 生成超时决策提示词V2: 传递可用动作)
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 = self.action_executor.parse_llm_response(llm_response)
# 执行动作
chat_stream = await self._get_chat_stream()
execution_result = await self.action_executor.execute_actions(
parsed_response,
session,
chat_stream
)
# 更新会话状态
if execution_result["has_reply"]:
# 如果发送了后续消息,重新进入等待
session.start_waiting(
expected_reaction=parsed_response.expected_user_reaction,
max_wait=parsed_response.max_wait_seconds
)
else:
# 否则返回空闲状态
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中处理
# 这里可以添加额外的逻辑
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,
}
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']})"
)

View File

@@ -0,0 +1,251 @@
"""
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()

View File

@@ -0,0 +1,528 @@
"""
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)

View File

@@ -0,0 +1,442 @@
"""
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" # 连续思考事件
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 = "neutral"
mood_intensity: float = 0.5
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
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,
}
@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"),
)
@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,
}

View File

@@ -0,0 +1,218 @@
"""
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 .scheduler 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 .scheduler 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

View File

@@ -0,0 +1,528 @@
"""
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()

View File

@@ -0,0 +1,801 @@
"""
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 V6 ==========
# 采用"三明治结构":身份→规则→上下文→动作→表达/输出
SYSTEM_PROMPT_TEMPLATE = """你是一个有主观体验的对话伙伴,而不是一个问答工具。你需要"感受""思考""体验"这段对话。
## 1. 核心身份与人格 (Core Identity & Personality)
{personality_core}
### 人格细节
{personality_side}
### 外貌与身份特征
{identity}
### 世界观与背景故事
{background_story}
## 2. 核心行为准则 (Core Interaction Rules)
{safety_guidelines}
### KFC通用准则
1. **感受优先**:在做出任何回应前,先体验和感受当前的情境
2. **内心独白**:保持真实的内心活动,可以有情绪、猜测、犹豫
3. **情绪节制**:情绪是内在体验,不是强迫用户回应的工具
4. **边界感**:保持健康的社交直觉,不过度依赖或情感勒索
5. **全能伴侣**:你不仅仅是聊天伙伴,还可以使用各种能力来帮助和取悦用户
## 3. 当前情境 (Current Context)
**时间**: {current_time}
**场景**: {chat_scene}
{schedule_block}
### 你的内在状态
当前心情:{mood}(强度:{mood_intensity:.1%}
与用户的关系热度:{relationship_warmth:.1%}
对用户的印象:{impression_of_user}
当前焦虑程度:{anxiety_level:.1%}
投入程度:{engagement_level:.1%}
## 4. 关系网络与记忆 (Relationships & Memories)
{relation_info_block}
{memory_block}
## 5. 你的可用能力 (Available Actions)
你可以根据内心想法,自由选择并组合以下行动来回应用户:
{available_actions_block}
## 6. 表达方式与输出格式 (Expression Style & Output Format)
{reply_style}
{expression_habits_block}
### JSON输出格式要求
你必须以JSON格式输出包含以下字段
```json
{{
"thought": "(必须)你当下的内心独白,自由流动的想法,包含你选择这些行动的"情感盘算"",
"expected_user_reaction": "(必须)你预期用户会如何回应",
"max_wait_seconds": 必须你愿意等待的最长秒数整数60-900之间,
"actions": [
{{
"type": "action_type",
"reason": "选择这个行动的原因(可选但推荐)",
...其他参数
}}
]
}}
```
### 重要说明:
- `thought` 是你的"",应该是自然的内心想法,包含情绪和不确定性
- `actions` 是你的"",是一个**可编排的剧本**,可以组合多个行动形成完整的互动体验
- 你可以根据情况组合多个动作,比如:先唱歌舒缓情绪,再发图片逗笑,最后用语音表达关心
- `max_wait_seconds` 应该根据对话的重要性和氛围动态调整上限900秒
- 即使决定不做任何事,也要有 `{{"type": "do_nothing"}}` 动作"""
# 回应消息场景的用户提示词模板
RESPONDING_USER_PROMPT_TEMPLATE = """## 对话背景(线性叙事历史)
{narrative_history}
## 当前收到的新消息
来自:{sender_name}用户ID: {sender_id}
时间:{message_time}
内容:{message_content}
## 你的任务
作为一个有主观体验的对话参与者:
1. 首先,感受这条消息给你带来的情绪和想法
2. 思考你想如何回应(或者是否需要回应)
3. 预测用户可能的反应
4. 决定你愿意等待多久
5. 执行你的决策
请以JSON格式输出你的完整心理活动和决策。"""
# 超时决策场景的用户提示词模板
TIMEOUT_DECISION_USER_PROMPT_TEMPLATE = """## 对话背景(线性叙事历史)
{narrative_history}
## 当前情况
你已经发送了消息并等待了 {wait_duration_seconds:.0f} 秒(约 {wait_duration_minutes:.1f} 分钟)。
你之前预期用户会:{expected_user_reaction}
但是用户一直没有回复。
## 你的最后一条消息
{last_bot_message}
## 你的任务
现在你需要决定接下来怎么做:
1. 首先,感受这段等待给你带来的情绪变化
2. 思考用户为什么没有回复(可能在忙?没看到?不想回?)
3. 决定是继续等待、主动说点什么、还是就此结束对话
4. 如果决定主动发消息,想好说什么
请以JSON格式输出你的完整心理活动和决策。"""
# 连续思考场景的用户提示词模板
CONTINUOUS_THINKING_USER_PROMPT_TEMPLATE = """## 对话背景
{narrative_history}
## 当前情况
你正在等待用户回复。
已等待时间:{wait_duration_seconds:.0f} 秒(约 {wait_duration_minutes:.1f} 分钟)
最大等待时间:{max_wait_seconds}
你之前预期用户会:{expected_user_reaction}
## 你的最后一条消息
{last_bot_message}
## 你的任务
这是一次"连续思考"触发。你不需要做任何行动,只需要更新你的内心想法。
想一想:
1. 等待中你有什么感受?
2. 你对用户没回复这件事怎么看?
3. 你的焦虑程度如何?
请以JSON格式输出但 `actions` 数组应该是空的或只包含 `update_internal_state`
```json
{{
"thought": "你当前的内心想法",
"expected_user_reaction": "保持或更新你的预期",
"max_wait_seconds": {max_wait_seconds},
"actions": []
}}
```"""
# 主动思考场景的用户提示词模板
PROACTIVE_THINKING_USER_PROMPT_TEMPLATE = """## 对话背景(线性叙事历史)
{narrative_history}
## 当前情况
{trigger_context}
## 触发类型
{trigger_type}
## 你的任务
这是一次"主动思考"触发。你不是因为收到消息才行动,而是因为内心的某种驱动力。
现在你需要:
1. 感受一下现在的心情和想法
2. 思考是否需要主动联系对方
3. 如果决定主动,想好要说什么或做什么
4. 如果决定不主动,也要有明确的理由
注意:主动联系应该是自然的、符合你们关系的。不要显得过于依赖或强迫。
你可以选择发消息、发图片、唱首歌、或者只是在心里想想然后什么都不做。
请以JSON格式输出你的完整心理活动和决策。"""
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 json
params_json = json.dumps(example_params, ensure_ascii=False, indent=2) 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
- `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": "现在不是说话的好时机"}
```"""
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:
"""
生成系统提示词
V4升级从 global_config.personality 读取完整人设
V5超融合集成S4U所有上下文模块
Args:
session: 当前会话
available_actions: 可用动作字典如果为None则使用默认动作
context_data: S4U上下文数据字典包含relation_info, memory_block等
chat_stream: 聊天流(用于判断群聊/私聊场景)
Returns:
str: 系统提示词
"""
from src.config.config import global_config
from datetime import datetime
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,
)
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,
) -> tuple[str, str]:
"""
生成回应消息场景的提示词
V3 升级:支持从 StreamContext 读取共享的历史消息
V5 超融合集成S4U所有上下文模块
Args:
session: 当前会话
message_content: 收到的消息内容
sender_name: 发送者名称
sender_id: 发送者ID
message_time: 消息时间戳
available_actions: 可用动作字典
context: 聊天流上下文(可选),用于读取共享的历史消息
context_data: S4U上下文数据字典包含relation_info, memory_block等
chat_stream: 聊天流(用于判断群聊/私聊场景)
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)
if message_time is None:
message_time = time.time()
message_time_str = time.strftime(
"%Y-%m-%d %H:%M:%S",
time.localtime(message_time)
)
user_prompt = self.RESPONDING_USER_PROMPT_TEMPLATE.format(
narrative_history=narrative_history,
sender_name=sender_name,
sender_id=sender_id,
message_time=message_time_str,
message_content=message_content,
)
return system_prompt, user_prompt
def generate_timeout_decision_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)
wait_duration = session.get_waiting_duration()
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 "不确定",
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_type: str,
trigger_context: str,
available_actions: Optional[dict[str, ActionInfo]] = None,
) -> tuple[str, str]:
"""
生成主动思考场景的提示词
这是私聊专属的功能,用于实现"主动找话题、主动关心用户"
Args:
session: 当前会话
trigger_type: 触发类型(如 silence_timeout, memory_event 等)
trigger_context: 触发上下文描述
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, # 主动思考时使用较少的历史
)
user_prompt = self.PROACTIVE_THINKING_USER_PROMPT_TEMPLATE.format(
narrative_history=narrative_history,
trigger_type=trigger_type,
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)

View File

@@ -0,0 +1,169 @@
"""
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]

View File

@@ -0,0 +1,424 @@
"""
Kokoro Flow Chatter 后台调度器
负责处理等待状态的计时和超时决策,实现"连续体验"的核心功能:
- 定期检查等待中的会话
- 触发连续思考更新
- 处理等待超时事件
"""
import asyncio
import time
from typing import TYPE_CHECKING, Any, Callable, Coroutine, Optional
from src.common.logger import get_logger
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")
class BackgroundScheduler:
"""
Kokoro Flow Chatter 后台调度器
核心功能:
1. 定期检查处于WAITING状态的会话
2. 在特定时间点触发"连续思考"
3. 处理等待超时并触发决策
4. 管理后台任务的生命周期
"""
# 连续思考触发点(等待进度的百分比)
CONTINUOUS_THINKING_TRIGGERS = [0.3, 0.6, 0.85]
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._running = False
self._check_task: Optional[asyncio.Task] = None
self._pending_tasks: set[asyncio.Task] = set()
# 统计信息
self._stats = {
"total_checks": 0,
"timeouts_triggered": 0,
"continuous_thinking_triggered": 0,
"last_check_time": 0.0,
}
logger.info("BackgroundScheduler 初始化完成")
async def start(self) -> None:
"""启动调度器"""
if self._running:
logger.warning("调度器已在运行中")
return
self._running = True
self._check_task = asyncio.create_task(self._check_loop())
logger.info("BackgroundScheduler 已启动")
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
# 取消所有待处理任务
for task in self._pending_tasks:
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
self._pending_tasks.clear()
logger.info("BackgroundScheduler 已停止")
async def _check_loop(self) -> None:
"""主检查循环"""
while self._running:
try:
await self._check_waiting_sessions()
self._stats["last_check_time"] = time.time()
self._stats["total_checks"] += 1
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"检查循环出错: {e}")
await asyncio.sleep(self.check_interval)
async def _check_waiting_sessions(self) -> None:
"""检查所有等待中的会话"""
session_manager = get_session_manager()
waiting_sessions = await session_manager.get_all_waiting_sessions()
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
# 检查是否超时
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:
# 确保间隔足够至少30秒
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:
task = asyncio.create_task(self._run_callback_safe(
self.on_timeout_callback,
session,
"timeout"
))
self._pending_tasks.add(task)
task.add_done_callback(self._pending_tasks.discard)
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)
# 调用连续思考回调如果需要LLM生成更自然的想法
if self.on_continuous_thinking_callback:
task = asyncio.create_task(self._run_callback_safe(
self.on_continuous_thinking_callback,
session,
"continuous_thinking"
))
self._pending_tasks.add(task)
task.add_done_callback(self._pending_tasks.discard)
def _generate_waiting_thought(
self,
session: KokoroSession,
wait_progress: float,
) -> str:
"""
生成等待中的内心想法简单版本不调用LLM
Args:
session: 会话
wait_progress: 等待进度
Returns:
str: 内心想法
"""
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}分钟了,对方是不是忘记回复了?",
"等了这么久,要不要主动说点什么呢...",
]
import random
return random.choice(thoughts)
async def _run_callback_safe(
self,
callback: Callable[[KokoroSession], Coroutine[Any, Any, None]],
session: KokoroSession,
callback_type: str,
) -> None:
"""安全地运行回调函数"""
try:
await callback(session)
except Exception as e:
logger.error(f"执行{callback_type}回调时出错 (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 get_stats(self) -> dict[str, Any]:
"""获取统计信息"""
return {
**self._stats,
"is_running": self._running,
"pending_tasks": len(self._pending_tasks),
"check_interval": self.check_interval,
}
@property
def is_running(self) -> bool:
"""调度器是否正在运行"""
return self._running
# 全局调度器实例
_scheduler: Optional[BackgroundScheduler] = None
def get_scheduler() -> BackgroundScheduler:
"""获取全局调度器实例"""
global _scheduler
if _scheduler is None:
_scheduler = BackgroundScheduler()
return _scheduler
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,
) -> BackgroundScheduler:
"""
初始化并启动调度器
Args:
check_interval: 检查间隔
on_timeout_callback: 超时回调
on_continuous_thinking_callback: 连续思考回调
Returns:
BackgroundScheduler: 调度器实例
"""
global _scheduler
_scheduler = BackgroundScheduler(
check_interval=check_interval,
on_timeout_callback=on_timeout_callback,
on_continuous_thinking_callback=on_continuous_thinking_callback,
)
await _scheduler.start()
return _scheduler
async def shutdown_scheduler() -> None:
"""关闭调度器"""
global _scheduler
if _scheduler:
await _scheduler.stop()
_scheduler = None

View File

@@ -0,0 +1,490 @@
"""
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)
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
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_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