feat(KFC): 为私人聊天实现主动思考
此提交为 Kokoro Flow Chatter(KFC)引入了主动思考机制,使其在私人聊天中在一段时间沉默后主动发起对话。此功能旨在通过模拟一个会主动思考并联系用户的角色,创造更自然和有趣的互动。 此功能的关键组成部分包括: - **主动思考回调**:`chatter.py` 中新增的回调处理整个主动思考流程,从上下文构建、提示生成到调用 LLM 和执行操作。 - **调度器集成**:`kfc_scheduler_adapter.py` 现在向 `UnifiedScheduler` 注册了一个定期任务,用于检查符合主动思考条件的会话。 - **触发条件**:主动思考的触发基于多个因素的组合,包括沉默时间、关系评分、概率检查以及“安静时间”,以避免打扰用户,系统现在获取全局关系分数以进行更准确的评估。- **专用提示**:`prompt_generator.py` 中的新提示专为此场景设计。它鼓励大语言模型决定是否发起联系,并将“不要打扰”作为一个有效且谨慎的选择。- **配置**:在 `bot_config_template.toml` 中添加了新设置以控制主动思维功能,包括静默阈值、最低亲和力和安静时间。- **AFC 集成**:通用的 `affinity_flow_chatter` 现在会检查 KFC 的主动思维是否在私人聊天中启用,并将控制权交给该功能,从而防止重复发送主动消息。
This commit is contained in:
@@ -699,6 +699,42 @@ async def execute_proactive_thinking(stream_id: str):
|
||||
|
||||
try:
|
||||
# 0. 前置检查
|
||||
|
||||
# 0.-1 检查是否是私聊且 KFC 主动思考已启用(让 KFC 接管私聊主动思考)
|
||||
try:
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
chat_manager = get_chat_manager()
|
||||
chat_stream = await chat_manager.get_stream(stream_id)
|
||||
|
||||
# 判断是否是私聊(使用 chat_type 枚举或从 stream_id 判断)
|
||||
is_private = False
|
||||
if chat_stream:
|
||||
try:
|
||||
is_private = chat_stream.chat_type.name == "private"
|
||||
except Exception:
|
||||
# 回退:从 stream_id 判断(私聊通常不包含 "group")
|
||||
is_private = "group" not in stream_id.lower()
|
||||
|
||||
if is_private:
|
||||
# 这是一个私聊,检查 KFC 是否启用且其主动思考是否启用
|
||||
try:
|
||||
from src.config.config import global_config
|
||||
kfc_config = getattr(global_config, 'kokoro_flow_chatter', None)
|
||||
if kfc_config:
|
||||
kfc_enabled = getattr(kfc_config, 'enable', False)
|
||||
proactive_config = getattr(kfc_config, 'proactive_thinking', None)
|
||||
proactive_enabled = getattr(proactive_config, 'enabled', False) if proactive_config else False
|
||||
|
||||
if kfc_enabled and proactive_enabled:
|
||||
logger.debug(
|
||||
f"[主动思考] 私聊 {stream_id} 由 KFC 主动思考接管,跳过通用主动思考"
|
||||
)
|
||||
return
|
||||
except Exception as e:
|
||||
logger.debug(f"检查 KFC 配置时出错,继续执行通用主动思考: {e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"检查私聊/KFC 状态时出错: {e},继续执行")
|
||||
|
||||
# 0.0 检查聊天流是否正在处理消息(双重保护)
|
||||
try:
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
|
||||
@@ -143,6 +143,12 @@ class KokoroFlowChatter(BaseChatter):
|
||||
self.scheduler.set_continuous_thinking_callback(
|
||||
self._on_continuous_thinking
|
||||
)
|
||||
|
||||
# 设置主动思考回调
|
||||
if self.enable_proactive:
|
||||
self.scheduler.set_proactive_thinking_callback(
|
||||
self._on_proactive_thinking
|
||||
)
|
||||
|
||||
async def execute(self, context: StreamContext) -> dict:
|
||||
"""
|
||||
@@ -573,6 +579,106 @@ class KokoroFlowChatter(BaseChatter):
|
||||
# 简单模式:更新焦虑程度(已在scheduler中处理)
|
||||
# 这里可以添加额外的逻辑
|
||||
|
||||
async def _on_proactive_thinking(self, session: KokoroSession, trigger_reason: str) -> None:
|
||||
"""
|
||||
主动思考回调
|
||||
|
||||
当长时间沉默后触发,让 LLM 决定是否主动联系用户。
|
||||
这不是"必须发消息",而是"想一想要不要联系对方"。
|
||||
|
||||
Args:
|
||||
session: 会话
|
||||
trigger_reason: 触发原因描述
|
||||
"""
|
||||
logger.info(f"[KFC] 处理主动思考: user={session.user_id}, reason={trigger_reason}")
|
||||
|
||||
try:
|
||||
# 创建正确的 ActionExecutor(使用 session 的 stream_id)
|
||||
from .action_executor import ActionExecutor
|
||||
proactive_action_executor = ActionExecutor(session.stream_id)
|
||||
|
||||
# 加载可用动作
|
||||
available_actions = await proactive_action_executor.load_actions()
|
||||
|
||||
# 获取 chat_stream 用于构建上下文
|
||||
chat_stream = await self._get_chat_stream(session.stream_id)
|
||||
|
||||
# 构建 S4U 上下文数据(包含全局关系信息)
|
||||
context_data: dict[str, str] = {}
|
||||
if chat_stream:
|
||||
try:
|
||||
from .context_builder import KFCContextBuilder
|
||||
context_builder = KFCContextBuilder(chat_stream)
|
||||
context_data = await context_builder.build_all_context(
|
||||
sender_name=session.user_id, # 主动思考时用 user_id
|
||||
target_message="", # 没有目标消息
|
||||
context=None,
|
||||
)
|
||||
logger.debug(f"[KFC] 主动思考上下文构建完成: {list(context_data.keys())}")
|
||||
except Exception as e:
|
||||
logger.warning(f"[KFC] 主动思考构建S4U上下文失败: {e}")
|
||||
|
||||
# 生成主动思考提示词(传入 context_data 以获取全局关系信息)
|
||||
system_prompt, user_prompt = self.prompt_generator.generate_proactive_thinking_prompt(
|
||||
session,
|
||||
trigger_context=trigger_reason,
|
||||
available_actions=available_actions,
|
||||
context_data=context_data,
|
||||
chat_stream=chat_stream,
|
||||
)
|
||||
|
||||
# 调用 LLM
|
||||
llm_response = await self._call_llm(system_prompt, user_prompt)
|
||||
self.stats["llm_calls"] += 1
|
||||
|
||||
# 解析响应
|
||||
parsed_response = proactive_action_executor.parse_llm_response(llm_response)
|
||||
|
||||
# 检查是否决定不打扰(do_nothing)
|
||||
is_do_nothing = (
|
||||
len(parsed_response.actions) == 0 or
|
||||
(len(parsed_response.actions) == 1 and parsed_response.actions[0].type == "do_nothing")
|
||||
)
|
||||
|
||||
if is_do_nothing:
|
||||
logger.info(f"[KFC] 主动思考决定不打扰: user={session.user_id}, thought={parsed_response.thought[:50]}...")
|
||||
# 记录这次"决定不打扰"的思考
|
||||
entry = MentalLogEntry(
|
||||
event_type=MentalLogEventType.PROACTIVE_THINKING,
|
||||
timestamp=time.time(),
|
||||
thought=parsed_response.thought,
|
||||
content="决定不打扰",
|
||||
emotional_snapshot=session.emotional_state.to_dict(),
|
||||
metadata={"trigger_reason": trigger_reason, "action": "do_nothing"},
|
||||
)
|
||||
session.add_mental_log_entry(entry)
|
||||
await self.session_manager.save_session(session.user_id)
|
||||
return
|
||||
|
||||
# 执行决定的动作
|
||||
execution_result = await proactive_action_executor.execute_actions(
|
||||
parsed_response,
|
||||
session,
|
||||
chat_stream
|
||||
)
|
||||
|
||||
logger.info(f"[KFC] 主动思考执行完成: user={session.user_id}, has_reply={execution_result.get('has_reply')}")
|
||||
|
||||
# 如果发送了消息,进入等待状态
|
||||
if execution_result.get("has_reply"):
|
||||
session.start_waiting(
|
||||
expected_reaction=parsed_response.expected_user_reaction,
|
||||
max_wait=parsed_response.max_wait_seconds
|
||||
)
|
||||
|
||||
# 保存会话
|
||||
await self.session_manager.save_session(session.user_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[KFC] 主动思考处理失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _build_result(
|
||||
self,
|
||||
success: bool,
|
||||
|
||||
@@ -5,16 +5,19 @@ Kokoro Flow Chatter 调度器适配器
|
||||
不再自己创建后台循环,而是复用全局调度器的基础设施。
|
||||
|
||||
核心功能:
|
||||
1. 会话等待超时检测
|
||||
2. 连续思考触发
|
||||
3. 与 UnifiedScheduler 的集成
|
||||
1. 会话等待超时检测(短期)
|
||||
2. 连续思考触发(等待期间的内心活动)
|
||||
3. 主动思考检测(长期沉默后主动发起对话)
|
||||
4. 与 UnifiedScheduler 的集成
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any, Callable, Coroutine, Optional
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import global_config
|
||||
from src.plugin_system.apis.unified_scheduler import (
|
||||
TriggerType,
|
||||
unified_scheduler,
|
||||
@@ -41,9 +44,10 @@ class KFCSchedulerAdapter:
|
||||
使用 UnifiedScheduler 实现 KFC 的定时任务功能,不再自行管理后台循环。
|
||||
|
||||
核心功能:
|
||||
1. 定期检查处于 WAITING 状态的会话
|
||||
2. 在特定时间点触发"连续思考"
|
||||
3. 处理等待超时并触发决策
|
||||
1. 定期检查处于 WAITING 状态的会话(短期等待超时)
|
||||
2. 在特定时间点触发"连续思考"(等待期间内心活动)
|
||||
3. 定期检查长期沉默的会话,触发"主动思考"(长期主动发起)
|
||||
4. 处理等待超时并触发决策
|
||||
"""
|
||||
|
||||
# 连续思考触发点(等待进度的百分比)
|
||||
@@ -51,45 +55,86 @@ class KFCSchedulerAdapter:
|
||||
|
||||
# 任务名称常量
|
||||
TASK_NAME_WAITING_CHECK = "kfc_waiting_check"
|
||||
TASK_NAME_PROACTIVE_CHECK = "kfc_proactive_check"
|
||||
|
||||
# 主动思考检查间隔(5分钟)
|
||||
PROACTIVE_CHECK_INTERVAL = 300.0
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
check_interval: float = 10.0,
|
||||
on_timeout_callback: Optional[Callable[[KokoroSession], Coroutine[Any, Any, None]]] = None,
|
||||
on_continuous_thinking_callback: Optional[Callable[[KokoroSession], Coroutine[Any, Any, None]]] = None,
|
||||
on_proactive_thinking_callback: Optional[Callable[[KokoroSession, str], Coroutine[Any, Any, None]]] = None,
|
||||
):
|
||||
"""
|
||||
初始化调度器适配器
|
||||
|
||||
Args:
|
||||
check_interval: 检查间隔(秒)
|
||||
check_interval: 等待检查间隔(秒)
|
||||
on_timeout_callback: 超时回调函数
|
||||
on_continuous_thinking_callback: 连续思考回调函数
|
||||
on_proactive_thinking_callback: 主动思考回调函数,接收 (session, trigger_reason)
|
||||
"""
|
||||
self.check_interval = check_interval
|
||||
self.on_timeout_callback = on_timeout_callback
|
||||
self.on_continuous_thinking_callback = on_continuous_thinking_callback
|
||||
self.on_proactive_thinking_callback = on_proactive_thinking_callback
|
||||
|
||||
self._registered = False
|
||||
self._schedule_id: Optional[str] = None
|
||||
self._proactive_schedule_id: Optional[str] = None
|
||||
|
||||
# 加载主动思考配置
|
||||
self._load_proactive_config()
|
||||
|
||||
# 统计信息
|
||||
self._stats = {
|
||||
"total_checks": 0,
|
||||
"timeouts_triggered": 0,
|
||||
"continuous_thinking_triggered": 0,
|
||||
"proactive_thinking_triggered": 0,
|
||||
"proactive_checks": 0,
|
||||
"last_check_time": 0.0,
|
||||
}
|
||||
|
||||
logger.info("KFCSchedulerAdapter 初始化完成")
|
||||
|
||||
def _load_proactive_config(self) -> None:
|
||||
"""加载主动思考相关配置"""
|
||||
try:
|
||||
if global_config and hasattr(global_config, 'kokoro_flow_chatter'):
|
||||
proactive_cfg = global_config.kokoro_flow_chatter.proactive_thinking
|
||||
self.proactive_enabled = proactive_cfg.enabled
|
||||
self.silence_threshold = proactive_cfg.silence_threshold_seconds
|
||||
self.min_interval = proactive_cfg.min_interval_between_proactive
|
||||
self.min_affinity = getattr(proactive_cfg, 'min_affinity_for_proactive', 0.3)
|
||||
self.quiet_hours_start = getattr(proactive_cfg, 'quiet_hours_start', "23:00")
|
||||
self.quiet_hours_end = getattr(proactive_cfg, 'quiet_hours_end', "07:00")
|
||||
else:
|
||||
# 默认值
|
||||
self.proactive_enabled = True
|
||||
self.silence_threshold = 7200 # 2小时
|
||||
self.min_interval = 1800 # 30分钟
|
||||
self.min_affinity = 0.3
|
||||
self.quiet_hours_start = "23:00"
|
||||
self.quiet_hours_end = "07:00"
|
||||
except Exception as e:
|
||||
logger.warning(f"加载主动思考配置失败,使用默认值: {e}")
|
||||
self.proactive_enabled = True
|
||||
self.silence_threshold = 7200
|
||||
self.min_interval = 1800
|
||||
self.min_affinity = 0.3
|
||||
self.quiet_hours_start = "23:00"
|
||||
self.quiet_hours_end = "07:00"
|
||||
|
||||
async def start(self) -> None:
|
||||
"""启动调度器(注册到 UnifiedScheduler)"""
|
||||
if self._registered:
|
||||
logger.warning("KFC 调度器已在运行中")
|
||||
return
|
||||
|
||||
# 注册周期性检查任务
|
||||
# 注册周期性等待检查任务(每10秒)
|
||||
self._schedule_id = await unified_scheduler.create_schedule(
|
||||
callback=self._check_waiting_sessions,
|
||||
trigger_type=TriggerType.TIME,
|
||||
@@ -97,9 +142,22 @@ class KFCSchedulerAdapter:
|
||||
is_recurring=True,
|
||||
task_name=self.TASK_NAME_WAITING_CHECK,
|
||||
force_overwrite=True,
|
||||
timeout=30.0, # 单次检查超时 30 秒
|
||||
timeout=30.0,
|
||||
)
|
||||
|
||||
# 如果启用了主动思考,注册主动思考检查任务(每5分钟)
|
||||
if self.proactive_enabled:
|
||||
self._proactive_schedule_id = await unified_scheduler.create_schedule(
|
||||
callback=self._check_proactive_sessions,
|
||||
trigger_type=TriggerType.TIME,
|
||||
trigger_config={"delay_seconds": self.PROACTIVE_CHECK_INTERVAL},
|
||||
is_recurring=True,
|
||||
task_name=self.TASK_NAME_PROACTIVE_CHECK,
|
||||
force_overwrite=True,
|
||||
timeout=120.0, # 主动思考可能需要更长时间(涉及 LLM 调用)
|
||||
)
|
||||
logger.info(f"KFC 主动思考调度已注册: schedule_id={self._proactive_schedule_id}")
|
||||
|
||||
self._registered = True
|
||||
logger.info(f"KFC 调度器已注册到 UnifiedScheduler: schedule_id={self._schedule_id}")
|
||||
|
||||
@@ -111,12 +169,16 @@ class KFCSchedulerAdapter:
|
||||
try:
|
||||
if self._schedule_id:
|
||||
await unified_scheduler.remove_schedule(self._schedule_id)
|
||||
logger.info(f"KFC 调度器已从 UnifiedScheduler 注销: schedule_id={self._schedule_id}")
|
||||
logger.info(f"KFC 等待检查调度已注销: schedule_id={self._schedule_id}")
|
||||
if self._proactive_schedule_id:
|
||||
await unified_scheduler.remove_schedule(self._proactive_schedule_id)
|
||||
logger.info(f"KFC 主动思考调度已注销: schedule_id={self._proactive_schedule_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"停止 KFC 调度器时出错: {e}")
|
||||
finally:
|
||||
self._registered = False
|
||||
self._schedule_id = None
|
||||
self._proactive_schedule_id = None
|
||||
|
||||
async def _check_waiting_sessions(self) -> None:
|
||||
"""检查所有等待中的会话(由 UnifiedScheduler 调用)
|
||||
@@ -344,6 +406,223 @@ class KFCSchedulerAdapter:
|
||||
|
||||
return random.choice(thoughts)
|
||||
|
||||
# ========================================
|
||||
# 主动思考相关方法(长期沉默后主动发起对话)
|
||||
# ========================================
|
||||
|
||||
async def _check_proactive_sessions(self) -> None:
|
||||
"""
|
||||
检查所有会话是否需要触发主动思考(由 UnifiedScheduler 定期调用)
|
||||
|
||||
主动思考的触发条件:
|
||||
1. 会话处于 IDLE 状态(不在等待回复中)
|
||||
2. 距离上次活动超过 silence_threshold
|
||||
3. 距离上次主动思考超过 min_interval
|
||||
4. 不在勿扰时段
|
||||
5. 与用户的关系亲密度足够
|
||||
"""
|
||||
if not self.proactive_enabled:
|
||||
return
|
||||
|
||||
# 检查是否在勿扰时段
|
||||
if self._is_quiet_hours():
|
||||
logger.debug("[KFC] 当前处于勿扰时段,跳过主动思考检查")
|
||||
return
|
||||
|
||||
self._stats["proactive_checks"] += 1
|
||||
|
||||
session_manager = get_session_manager()
|
||||
all_sessions = await session_manager.get_all_sessions()
|
||||
|
||||
current_time = time.time()
|
||||
|
||||
for session in all_sessions:
|
||||
try:
|
||||
# 检查是否满足主动思考条件(异步获取全局关系分数)
|
||||
trigger_reason = await self._should_trigger_proactive(session, current_time)
|
||||
if trigger_reason:
|
||||
logger.info(
|
||||
f"[KFC] 触发主动思考: user={session.user_id}, reason={trigger_reason}"
|
||||
)
|
||||
await self._handle_proactive_thinking(session, trigger_reason)
|
||||
except Exception as e:
|
||||
logger.error(f"检查主动思考条件时出错 (user={session.user_id}): {e}")
|
||||
|
||||
def _is_quiet_hours(self) -> bool:
|
||||
"""
|
||||
检查当前是否处于勿扰时段
|
||||
|
||||
支持跨午夜的时段(如 23:00 到 07:00)
|
||||
"""
|
||||
try:
|
||||
now = datetime.now()
|
||||
current_minutes = now.hour * 60 + now.minute
|
||||
|
||||
# 解析开始时间
|
||||
start_parts = self.quiet_hours_start.split(":")
|
||||
start_minutes = int(start_parts[0]) * 60 + int(start_parts[1])
|
||||
|
||||
# 解析结束时间
|
||||
end_parts = self.quiet_hours_end.split(":")
|
||||
end_minutes = int(end_parts[0]) * 60 + int(end_parts[1])
|
||||
|
||||
# 处理跨午夜的情况
|
||||
if start_minutes <= end_minutes:
|
||||
# 不跨午夜(如 09:00 到 17:00)
|
||||
return start_minutes <= current_minutes < end_minutes
|
||||
else:
|
||||
# 跨午夜(如 23:00 到 07:00)
|
||||
return current_minutes >= start_minutes or current_minutes < end_minutes
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"解析勿扰时段配置失败: {e}")
|
||||
return False
|
||||
|
||||
async def _should_trigger_proactive(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
current_time: float
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
检查是否应该触发主动思考
|
||||
|
||||
使用全局关系数据库中的关系分数(而不是 KFC 内部的 emotional_state)
|
||||
|
||||
概率机制:关系越亲密,触发概率越高
|
||||
- 亲密度 0.3 → 触发概率 10%
|
||||
- 亲密度 0.5 → 触发概率 30%
|
||||
- 亲密度 0.7 → 触发概率 55%
|
||||
- 亲密度 1.0 → 触发概率 90%
|
||||
|
||||
Args:
|
||||
session: 会话
|
||||
current_time: 当前时间戳
|
||||
|
||||
Returns:
|
||||
触发原因字符串,如果不触发则返回 None
|
||||
"""
|
||||
import random
|
||||
|
||||
# 条件1:必须处于 IDLE 状态
|
||||
if session.status != SessionStatus.IDLE:
|
||||
return None
|
||||
|
||||
# 条件2:距离上次活动超过沉默阈值
|
||||
silence_duration = current_time - session.last_activity_at
|
||||
if silence_duration < self.silence_threshold:
|
||||
return None
|
||||
|
||||
# 条件3:距离上次主动思考超过最小间隔
|
||||
if session.last_proactive_at is not None:
|
||||
time_since_last_proactive = current_time - session.last_proactive_at
|
||||
if time_since_last_proactive < self.min_interval:
|
||||
return None
|
||||
|
||||
# 条件4:从数据库获取全局关系分数
|
||||
relationship_score = await self._get_global_relationship_score(session.user_id)
|
||||
if relationship_score < self.min_affinity:
|
||||
logger.debug(
|
||||
f"主动思考跳过(关系分数不足): user={session.user_id}, "
|
||||
f"score={relationship_score:.2f}, min={self.min_affinity:.2f}"
|
||||
)
|
||||
return None
|
||||
|
||||
# 条件5:基于关系分数的概率判断
|
||||
# 公式:probability = 0.1 + 0.8 * ((score - min_affinity) / (1.0 - min_affinity))^1.5
|
||||
# 这样分数从 min_affinity 到 1.0 映射到概率 10% 到 90%
|
||||
# 使用1.5次幂让曲线更陡峭,高亲密度时概率增长更快
|
||||
normalized_score = (relationship_score - self.min_affinity) / (1.0 - self.min_affinity)
|
||||
probability = 0.1 + 0.8 * (normalized_score ** 1.5)
|
||||
probability = min(probability, 0.9) # 最高90%,永远不是100%确定
|
||||
|
||||
if random.random() > probability:
|
||||
# 这次检查没触发,但记录一下(用于调试)
|
||||
logger.debug(
|
||||
f"主动思考概率检查未通过: user={session.user_id}, "
|
||||
f"score={relationship_score:.2f}, probability={probability:.1%}"
|
||||
)
|
||||
return None
|
||||
|
||||
# 所有条件满足,生成触发原因
|
||||
silence_hours = silence_duration / 3600
|
||||
logger.info(
|
||||
f"主动思考触发: user={session.user_id}, "
|
||||
f"silence={silence_hours:.1f}h, score={relationship_score:.2f}, prob={probability:.1%}"
|
||||
)
|
||||
return f"沉默了{silence_hours:.1f}小时,想主动关心一下对方"
|
||||
|
||||
async def _get_global_relationship_score(self, user_id: str) -> float:
|
||||
"""
|
||||
从全局关系数据库获取关系分数
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
关系分数 (0.0-1.0),如果没有记录返回默认值 0.3
|
||||
"""
|
||||
try:
|
||||
from src.common.database.api.specialized import get_user_relationship
|
||||
|
||||
# 从 user_id 解析 platform(格式通常是 "platform_userid")
|
||||
# 这里假设 user_id 中包含 platform 信息,需要根据实际情况调整
|
||||
# 先尝试直接查询,如果失败再用默认值
|
||||
relationship = await get_user_relationship(
|
||||
platform="qq", # TODO: 从 session 或 stream_id 获取真实 platform
|
||||
user_id=user_id,
|
||||
target_id="bot",
|
||||
)
|
||||
|
||||
if relationship and hasattr(relationship, 'relationship_score'):
|
||||
return relationship.relationship_score
|
||||
|
||||
# 没有找到关系记录,返回默认值
|
||||
return 0.3
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"获取全局关系分数失败 (user={user_id}): {e}")
|
||||
return 0.3 # 出错时返回较低的默认值
|
||||
|
||||
async def _handle_proactive_thinking(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
trigger_reason: str
|
||||
) -> None:
|
||||
"""
|
||||
处理主动思考
|
||||
|
||||
Args:
|
||||
session: 会话
|
||||
trigger_reason: 触发原因
|
||||
"""
|
||||
self._stats["proactive_thinking_triggered"] += 1
|
||||
|
||||
# 更新会话状态
|
||||
session.last_proactive_at = time.time()
|
||||
session.proactive_count += 1
|
||||
|
||||
# 添加主动思考日志
|
||||
proactive_entry = MentalLogEntry(
|
||||
event_type=MentalLogEventType.PROACTIVE_THINKING,
|
||||
timestamp=time.time(),
|
||||
thought=trigger_reason,
|
||||
content="主动思考触发",
|
||||
emotional_snapshot=session.emotional_state.to_dict(),
|
||||
metadata={"trigger_reason": trigger_reason},
|
||||
)
|
||||
session.add_mental_log_entry(proactive_entry)
|
||||
|
||||
# 保存会话状态
|
||||
session_manager = get_session_manager()
|
||||
await session_manager.save_session(session.user_id)
|
||||
|
||||
# 调用主动思考回调(由 chatter 处理实际的 LLM 调用和动作执行)
|
||||
if self.on_proactive_thinking_callback:
|
||||
try:
|
||||
await self.on_proactive_thinking_callback(session, trigger_reason)
|
||||
except Exception as e:
|
||||
logger.error(f"执行主动思考回调时出错 (user={session.user_id}): {e}")
|
||||
|
||||
def set_timeout_callback(
|
||||
self,
|
||||
callback: Callable[[KokoroSession], Coroutine[Any, Any, None]],
|
||||
@@ -358,6 +637,13 @@ class KFCSchedulerAdapter:
|
||||
"""设置连续思考回调函数"""
|
||||
self.on_continuous_thinking_callback = callback
|
||||
|
||||
def set_proactive_thinking_callback(
|
||||
self,
|
||||
callback: Callable[[KokoroSession, str], Coroutine[Any, Any, None]],
|
||||
) -> None:
|
||||
"""设置主动思考回调函数"""
|
||||
self.on_proactive_thinking_callback = callback
|
||||
|
||||
def get_stats(self) -> dict[str, Any]:
|
||||
"""获取统计信息"""
|
||||
return {
|
||||
@@ -388,6 +674,7 @@ async def initialize_scheduler(
|
||||
check_interval: float = 10.0,
|
||||
on_timeout_callback: Optional[Callable[[KokoroSession], Coroutine[Any, Any, None]]] = None,
|
||||
on_continuous_thinking_callback: Optional[Callable[[KokoroSession], Coroutine[Any, Any, None]]] = None,
|
||||
on_proactive_thinking_callback: Optional[Callable[[KokoroSession, str], Coroutine[Any, Any, None]]] = None,
|
||||
) -> KFCSchedulerAdapter:
|
||||
"""
|
||||
初始化并启动调度器
|
||||
@@ -396,6 +683,7 @@ async def initialize_scheduler(
|
||||
check_interval: 检查间隔
|
||||
on_timeout_callback: 超时回调
|
||||
on_continuous_thinking_callback: 连续思考回调
|
||||
on_proactive_thinking_callback: 主动思考回调
|
||||
|
||||
Returns:
|
||||
KFCSchedulerAdapter: 调度器适配器实例
|
||||
@@ -405,6 +693,7 @@ async def initialize_scheduler(
|
||||
check_interval=check_interval,
|
||||
on_timeout_callback=on_timeout_callback,
|
||||
on_continuous_thinking_callback=on_continuous_thinking_callback,
|
||||
on_proactive_thinking_callback=on_proactive_thinking_callback,
|
||||
)
|
||||
await _scheduler_adapter.start()
|
||||
return _scheduler_adapter
|
||||
|
||||
@@ -47,6 +47,7 @@ class MentalLogEventType(Enum):
|
||||
TIMEOUT_DECISION = "timeout_decision" # 超时决策事件
|
||||
STATE_CHANGE = "state_change" # 状态变更事件
|
||||
CONTINUOUS_THINKING = "continuous_thinking" # 连续思考事件
|
||||
PROACTIVE_THINKING = "proactive_thinking" # 主动思考事件(长期沉默后主动发起)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
@@ -222,6 +223,10 @@ class KokoroSession:
|
||||
continuous_thinking_count: int = 0
|
||||
last_continuous_thinking_at: Optional[float] = None
|
||||
|
||||
# 主动思考相关(长期沉默后主动发起对话)
|
||||
last_proactive_at: Optional[float] = None # 上次主动思考的时间
|
||||
proactive_count: int = 0 # 主动思考的次数(累计)
|
||||
|
||||
def add_mental_log_entry(self, entry: MentalLogEntry, max_log_size: int = 100) -> None:
|
||||
"""
|
||||
添加心理活动日志条目
|
||||
@@ -284,6 +289,8 @@ class KokoroSession:
|
||||
"total_interactions": self.total_interactions,
|
||||
"continuous_thinking_count": self.continuous_thinking_count,
|
||||
"last_continuous_thinking_at": self.last_continuous_thinking_at,
|
||||
"last_proactive_at": self.last_proactive_at,
|
||||
"proactive_count": self.proactive_count,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@@ -320,6 +327,8 @@ class KokoroSession:
|
||||
total_interactions=data.get("total_interactions", 0),
|
||||
continuous_thinking_count=data.get("continuous_thinking_count", 0),
|
||||
last_continuous_thinking_at=data.get("last_continuous_thinking_at"),
|
||||
last_proactive_at=data.get("last_proactive_at"),
|
||||
proactive_count=data.get("proactive_count", 0),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -205,23 +205,31 @@ class PromptGenerator:
|
||||
{narrative_history}
|
||||
|
||||
## 当前情况
|
||||
现在是 {current_time}。
|
||||
距离你们上次对话已经过去了 {silence_duration}。
|
||||
|
||||
{relation_block}
|
||||
|
||||
{trigger_context}
|
||||
|
||||
## 触发类型
|
||||
{trigger_type}
|
||||
|
||||
## 你的任务
|
||||
这是一次"主动思考"触发。你不是因为收到消息才行动,而是因为内心的某种驱动力。
|
||||
现在你需要:
|
||||
1. 感受一下现在的心情和想法
|
||||
2. 思考是否需要主动联系对方
|
||||
3. 如果决定主动,想好要说什么或做什么
|
||||
4. 如果决定不主动,也要有明确的理由
|
||||
这是一次"主动思考"——你突然想起了对方,想了想要不要联系ta。
|
||||
|
||||
注意:主动联系应该是自然的、符合你们关系的。不要显得过于依赖或强迫。
|
||||
你可以选择发消息、发图片、唱首歌、或者只是在心里想想然后什么都不做。
|
||||
请像真人一样思考:
|
||||
1. **感受一下此刻的心情**:你真的想联系对方吗?还是只是习惯性地想起?
|
||||
2. **考虑对方的情况**:现在这个时间,对方可能在干嘛?方便被打扰吗?
|
||||
3. **回忆上次对话**:你们聊了什么?对话是怎么结束的?
|
||||
4. **做出决定**:
|
||||
- 如果决定联系:想好说什么,要自然,不要刻意
|
||||
- 如果决定不联系:这也完全ok,不打扰也是一种温柔
|
||||
|
||||
请以JSON格式输出你的完整心理活动和决策。"""
|
||||
**重要提醒**:
|
||||
- 你不是必须发消息的,"算了,不打扰了"是完全合理的选择
|
||||
- 如果决定联系,内容要自然——可以是分享、关心、延续话题,但不要生硬
|
||||
- 避免机械式的问候(如固定的"早安""晚安"),除非你们的关系真的会这样打招呼
|
||||
|
||||
请以JSON格式输出你的完整心理活动和决策。
|
||||
如果决定不打扰,actions 里放一个 `{{"type": "do_nothing"}}` 就好。"""
|
||||
|
||||
def __init__(self, persona_description: str = ""):
|
||||
"""
|
||||
@@ -663,34 +671,72 @@ class PromptGenerator:
|
||||
def generate_proactive_thinking_prompt(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
trigger_type: str,
|
||||
trigger_context: str,
|
||||
available_actions: Optional[dict[str, ActionInfo]] = None,
|
||||
context_data: Optional[dict[str, str]] = None,
|
||||
chat_stream: Optional["ChatStream"] = None,
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
生成主动思考场景的提示词
|
||||
|
||||
这是私聊专属的功能,用于实现"主动找话题、主动关心用户"。
|
||||
主动思考不是"必须发消息",而是"想一想要不要联系对方"。
|
||||
|
||||
Args:
|
||||
session: 当前会话
|
||||
trigger_type: 触发类型(如 silence_timeout, memory_event 等)
|
||||
trigger_context: 触发上下文描述
|
||||
trigger_context: 触发上下文描述(如"沉默了2小时")
|
||||
available_actions: 可用动作字典
|
||||
context_data: S4U上下文数据(包含全局关系信息)
|
||||
chat_stream: 聊天流
|
||||
|
||||
Returns:
|
||||
tuple[str, str]: (系统提示词, 用户提示词)
|
||||
"""
|
||||
system_prompt = self.generate_system_prompt(session, available_actions)
|
||||
from datetime import datetime
|
||||
import time
|
||||
|
||||
# 生成系统提示词(使用 context_data 获取完整的关系和记忆信息)
|
||||
system_prompt = self.generate_system_prompt(
|
||||
session,
|
||||
available_actions,
|
||||
context_data=context_data,
|
||||
chat_stream=chat_stream,
|
||||
)
|
||||
|
||||
narrative_history = self._format_narrative_history(
|
||||
session.mental_log,
|
||||
max_entries=10, # 主动思考时使用较少的历史
|
||||
)
|
||||
|
||||
# 计算沉默时长
|
||||
silence_seconds = time.time() - session.last_activity_at
|
||||
if silence_seconds < 3600:
|
||||
silence_duration = f"{silence_seconds / 60:.0f}分钟"
|
||||
else:
|
||||
silence_duration = f"{silence_seconds / 3600:.1f}小时"
|
||||
|
||||
# 当前时间
|
||||
current_time = datetime.now().strftime("%Y年%m月%d日 %H:%M")
|
||||
|
||||
# 从 context_data 获取全局关系信息(这是正确的来源)
|
||||
relation_block = ""
|
||||
if context_data:
|
||||
relation_info = context_data.get("relation_info", "")
|
||||
if relation_info:
|
||||
relation_block = f"### 你与对方的关系\n{relation_info}"
|
||||
|
||||
if not relation_block:
|
||||
# 回退:使用 session 的情感状态(不太准确但有总比没有好)
|
||||
es = session.emotional_state
|
||||
relation_block = f"""### 你与对方的关系
|
||||
- 当前心情:{es.mood}
|
||||
- 对对方的印象:{es.impression_of_user or "还在慢慢了解中"}"""
|
||||
|
||||
user_prompt = self.PROACTIVE_THINKING_USER_PROMPT_TEMPLATE.format(
|
||||
narrative_history=narrative_history,
|
||||
trigger_type=trigger_type,
|
||||
current_time=current_time,
|
||||
silence_duration=silence_duration,
|
||||
relation_block=relation_block,
|
||||
trigger_context=trigger_context,
|
||||
)
|
||||
|
||||
|
||||
@@ -421,6 +421,17 @@ class SessionManager:
|
||||
|
||||
return waiting_sessions
|
||||
|
||||
async def get_all_sessions(self) -> list[KokoroSession]:
|
||||
"""
|
||||
获取所有内存中的会话
|
||||
|
||||
用于主动思考检查等需要遍历所有会话的场景
|
||||
|
||||
Returns:
|
||||
list[KokoroSession]: 所有会话列表
|
||||
"""
|
||||
return list(self._sessions.values())
|
||||
|
||||
async def get_session_statistics(self) -> dict:
|
||||
"""
|
||||
获取会话统计信息
|
||||
|
||||
Reference in New Issue
Block a user