Merge branch 'feature/kfc' of https://github.com/MoFox-Studio/MoFox-Core into feature/kfc
This commit is contained in:
@@ -19,7 +19,8 @@ from .models import (
|
||||
)
|
||||
from .session import KokoroSession, SessionManager, get_session_manager
|
||||
from .chatter import KokoroFlowChatter
|
||||
from .replyer import generate_response
|
||||
from .planner import generate_plan
|
||||
from .replyer import generate_reply_text
|
||||
from .proactive_thinker import (
|
||||
ProactiveThinker,
|
||||
get_proactive_thinker,
|
||||
@@ -60,7 +61,8 @@ __all__ = [
|
||||
"get_session_manager",
|
||||
# Core Components
|
||||
"KokoroFlowChatter",
|
||||
"generate_response",
|
||||
"generate_plan",
|
||||
"generate_reply_text",
|
||||
# Proactive Thinker
|
||||
"ProactiveThinker",
|
||||
"get_proactive_thinker",
|
||||
|
||||
@@ -19,7 +19,8 @@ from src.plugin_system.base.base_chatter import BaseChatter
|
||||
from src.plugin_system.base.component_types import ChatType
|
||||
|
||||
from .models import SessionStatus
|
||||
from .replyer import generate_response
|
||||
from .planner import generate_plan
|
||||
from .replyer import generate_reply_text
|
||||
from .session import get_session_manager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -143,8 +144,8 @@ class KokoroFlowChatter(BaseChatter):
|
||||
# 8. 获取聊天流
|
||||
chat_stream = await self._get_chat_stream()
|
||||
|
||||
# 9. 调用 Replyer 生成响应
|
||||
response = await generate_response(
|
||||
# 9. 调用 Planner 生成行动计划
|
||||
plan_response = await generate_plan(
|
||||
session=session,
|
||||
user_name=user_name,
|
||||
situation_type=situation_type,
|
||||
@@ -152,15 +153,35 @@ class KokoroFlowChatter(BaseChatter):
|
||||
available_actions=available_actions,
|
||||
)
|
||||
|
||||
# 10. 执行动作作
|
||||
# 10. 对于需要回复的动作,调用 Replyer 生成实际文本
|
||||
processed_actions = []
|
||||
for action in plan_response.actions:
|
||||
if action.type == "kfc_reply":
|
||||
# 调用 replyer 生成回复文本
|
||||
success, reply_text = await generate_reply_text(
|
||||
session=session,
|
||||
user_name=user_name,
|
||||
thought=plan_response.thought,
|
||||
situation_type=situation_type,
|
||||
chat_stream=chat_stream,
|
||||
)
|
||||
if success and reply_text:
|
||||
# 更新 action 的 content
|
||||
action.params["content"] = reply_text
|
||||
else:
|
||||
logger.warning("[KFC] 回复生成失败,跳过该动作")
|
||||
continue
|
||||
processed_actions.append(action)
|
||||
|
||||
# 11. 执行动作
|
||||
exec_results = []
|
||||
has_reply = False
|
||||
for action in response.actions:
|
||||
for action in processed_actions:
|
||||
result = await self.action_manager.execute_action(
|
||||
action_name=action.type,
|
||||
chat_id=self.stream_id,
|
||||
target_message=target_message,
|
||||
reasoning=response.thought,
|
||||
reasoning=plan_response.thought,
|
||||
action_data=action.params,
|
||||
thinking_id=None,
|
||||
log_prefix="[KFC]",
|
||||
@@ -169,31 +190,31 @@ class KokoroFlowChatter(BaseChatter):
|
||||
if result.get("success") and action.type in ("kfc_reply", "respond"):
|
||||
has_reply = True
|
||||
|
||||
# 11. 记录 Bot 规划到 mental_log
|
||||
# 12. 记录 Bot 规划到 mental_log
|
||||
session.add_bot_planning(
|
||||
thought=response.thought,
|
||||
actions=[a.to_dict() for a in response.actions],
|
||||
expected_reaction=response.expected_reaction,
|
||||
max_wait_seconds=response.max_wait_seconds,
|
||||
thought=plan_response.thought,
|
||||
actions=[a.to_dict() for a in processed_actions],
|
||||
expected_reaction=plan_response.expected_reaction,
|
||||
max_wait_seconds=plan_response.max_wait_seconds,
|
||||
)
|
||||
|
||||
# 12. 更新 Session 状态
|
||||
if response.max_wait_seconds > 0:
|
||||
# 13. 更新 Session 状态
|
||||
if plan_response.max_wait_seconds > 0:
|
||||
session.start_waiting(
|
||||
expected_reaction=response.expected_reaction,
|
||||
max_wait_seconds=response.max_wait_seconds,
|
||||
expected_reaction=plan_response.expected_reaction,
|
||||
max_wait_seconds=plan_response.max_wait_seconds,
|
||||
)
|
||||
else:
|
||||
session.end_waiting()
|
||||
|
||||
# 13. 标记消息为已读
|
||||
# 14. 标记消息为已读
|
||||
for msg in unread_messages:
|
||||
context.mark_message_as_read(str(msg.message_id))
|
||||
|
||||
# 14. 保存 Session
|
||||
# 15. 保存 Session
|
||||
await self.session_manager.save_session(user_id)
|
||||
|
||||
# 15. 更新统计
|
||||
# 16. 更新统计
|
||||
self._stats["messages_processed"] += len(unread_messages)
|
||||
if has_reply:
|
||||
self._stats["successful_responses"] += 1
|
||||
@@ -201,15 +222,15 @@ class KokoroFlowChatter(BaseChatter):
|
||||
logger.info(
|
||||
f"{SOFT_PURPLE}[KFC]{RESET} 处理完成: "
|
||||
f"user={user_name}, situation={situation_type}, "
|
||||
f"actions={[a.type for a in response.actions]}, "
|
||||
f"wait={response.max_wait_seconds}s"
|
||||
f"actions={[a.type for a in processed_actions]}, "
|
||||
f"wait={plan_response.max_wait_seconds}s"
|
||||
)
|
||||
|
||||
return self._build_result(
|
||||
success=True,
|
||||
message="processed",
|
||||
has_reply=has_reply,
|
||||
thought=response.thought,
|
||||
thought=plan_response.thought,
|
||||
situation_type=situation_type,
|
||||
)
|
||||
|
||||
|
||||
112
src/plugins/built_in/kokoro_flow_chatter/planner.py
Normal file
112
src/plugins/built_in/kokoro_flow_chatter/planner.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
Kokoro Flow Chatter - Planner
|
||||
|
||||
规划器:负责分析情境并生成行动计划
|
||||
- 输入:会话状态、用户消息、情境类型
|
||||
- 输出:LLMResponse(包含 thought、actions、expected_reaction、max_wait_seconds)
|
||||
- 不负责生成具体回复文本,只决定"要做什么"
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.plugin_system.apis import llm_api
|
||||
from src.utils.json_parser import extract_and_parse_json
|
||||
|
||||
from .models import LLMResponse
|
||||
from .prompt.builder import get_prompt_builder
|
||||
from .session import KokoroSession
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.chat.message_receive.chat_stream import ChatStream
|
||||
|
||||
logger = get_logger("kfc_planner")
|
||||
|
||||
|
||||
async def generate_plan(
|
||||
session: KokoroSession,
|
||||
user_name: str,
|
||||
situation_type: str = "new_message",
|
||||
chat_stream: Optional["ChatStream"] = None,
|
||||
available_actions: Optional[dict] = None,
|
||||
extra_context: Optional[dict] = None,
|
||||
) -> LLMResponse:
|
||||
"""
|
||||
生成行动计划
|
||||
|
||||
Args:
|
||||
session: 会话对象
|
||||
user_name: 用户名称
|
||||
situation_type: 情况类型
|
||||
chat_stream: 聊天流对象
|
||||
available_actions: 可用动作字典
|
||||
extra_context: 额外上下文
|
||||
|
||||
Returns:
|
||||
LLMResponse 对象,包含计划信息
|
||||
"""
|
||||
try:
|
||||
# 1. 构建规划器提示词
|
||||
prompt_builder = get_prompt_builder()
|
||||
prompt = await prompt_builder.build_planner_prompt(
|
||||
session=session,
|
||||
user_name=user_name,
|
||||
situation_type=situation_type,
|
||||
chat_stream=chat_stream,
|
||||
available_actions=available_actions,
|
||||
extra_context=extra_context,
|
||||
)
|
||||
|
||||
from src.config.config import global_config
|
||||
if global_config and global_config.debug.show_prompt:
|
||||
logger.info(f"[KFC Planner] 生成的规划提示词:\n{prompt}")
|
||||
|
||||
# 2. 获取 planner 模型配置并调用 LLM
|
||||
models = llm_api.get_available_models()
|
||||
planner_config = models.get("planner")
|
||||
|
||||
if not planner_config:
|
||||
logger.error("[KFC Planner] 未找到 planner 模型配置")
|
||||
return LLMResponse.create_error_response("未找到 planner 模型配置")
|
||||
|
||||
success, raw_response, reasoning, model_name = await llm_api.generate_with_model(
|
||||
prompt=prompt,
|
||||
model_config=planner_config,
|
||||
request_type="kokoro_flow_chatter.plan",
|
||||
)
|
||||
|
||||
if not success:
|
||||
logger.error(f"[KFC Planner] LLM 调用失败: {raw_response}")
|
||||
return LLMResponse.create_error_response(raw_response)
|
||||
|
||||
logger.debug(f"[KFC Planner] LLM 响应 (model={model_name}):\n{raw_response}")
|
||||
|
||||
# 3. 解析响应
|
||||
return _parse_response(raw_response)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[KFC Planner] 生成失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return LLMResponse.create_error_response(str(e))
|
||||
|
||||
|
||||
def _parse_response(raw_response: str) -> LLMResponse:
|
||||
"""解析 LLM 响应"""
|
||||
data = extract_and_parse_json(raw_response, strict=False)
|
||||
|
||||
if not data or not isinstance(data, dict):
|
||||
logger.warning(f"[KFC Planner] 无法解析 JSON: {raw_response[:200]}...")
|
||||
return LLMResponse.create_error_response("无法解析响应格式")
|
||||
|
||||
response = LLMResponse.from_dict(data)
|
||||
|
||||
if response.thought:
|
||||
logger.info(
|
||||
f"[KFC Planner] 解析成功: thought={response.thought[:50]}..., "
|
||||
f"actions={[a.type for a in response.actions]}"
|
||||
)
|
||||
else:
|
||||
logger.warning("[KFC Planner] 响应缺少 thought")
|
||||
|
||||
return response
|
||||
@@ -21,7 +21,8 @@ from src.config.config import global_config
|
||||
from src.plugin_system.apis.unified_scheduler import TriggerType, unified_scheduler
|
||||
|
||||
from .models import EventType, SessionStatus
|
||||
from .replyer import generate_response
|
||||
from .planner import generate_plan
|
||||
from .replyer import _clean_reply_text, generate_reply_text
|
||||
from .session import KokoroSession, get_session_manager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -210,13 +211,16 @@ class ProactiveThinker:
|
||||
"""处理连续思考"""
|
||||
self._stats["continuous_thinking_triggered"] += 1
|
||||
|
||||
# 生成等待中的想法
|
||||
thought = self._generate_waiting_thought(session, progress)
|
||||
# 获取用户名
|
||||
user_name = await self._get_user_name(session.user_id, session.stream_id)
|
||||
|
||||
# 调用 LLM 生成等待中的想法
|
||||
thought = await self._generate_waiting_thought(session, user_name, progress)
|
||||
|
||||
# 记录到 mental_log
|
||||
session.add_waiting_update(
|
||||
waiting_thought=thought,
|
||||
mood="", # 可以根据进度设置心情
|
||||
mood="", # 心情已融入 thought 中
|
||||
)
|
||||
|
||||
# 更新思考计数
|
||||
@@ -231,10 +235,104 @@ class ProactiveThinker:
|
||||
f"progress={progress:.1%}, thought={thought[:30]}..."
|
||||
)
|
||||
|
||||
def _generate_waiting_thought(self, session: KokoroSession, progress: float) -> str:
|
||||
"""生成等待中的想法"""
|
||||
elapsed_minutes = session.waiting_config.get_elapsed_minutes()
|
||||
|
||||
async def _generate_waiting_thought(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
user_name: str,
|
||||
progress: float,
|
||||
) -> str:
|
||||
"""调用 LLM 生成等待中的想法"""
|
||||
try:
|
||||
from src.chat.utils.prompt import global_prompt_manager
|
||||
from src.plugin_system.apis import llm_api
|
||||
|
||||
from .prompt.builder import get_prompt_builder
|
||||
from .prompt.prompts import PROMPT_NAMES
|
||||
|
||||
# 使用 PromptBuilder 构建人设块
|
||||
prompt_builder = get_prompt_builder()
|
||||
persona_block = prompt_builder._build_persona_block()
|
||||
|
||||
# 获取关系信息
|
||||
relation_block = f"你与 {user_name} 还不太熟悉。"
|
||||
try:
|
||||
from src.person_info.relationship_manager import relationship_manager
|
||||
|
||||
person_info_manager = await self._get_person_info_manager()
|
||||
if person_info_manager:
|
||||
platform = global_config.bot.platform if global_config else "qq"
|
||||
person_id = person_info_manager.get_person_id(platform, session.user_id)
|
||||
relationship = await relationship_manager.get_relationship(person_id)
|
||||
if relationship:
|
||||
relation_block = f"你与 {user_name} 的亲密度是 {relationship.intimacy}。{relationship.description or ''}"
|
||||
except Exception as e:
|
||||
logger.debug(f"获取关系信息失败: {e}")
|
||||
|
||||
# 获取上次发送的消息
|
||||
last_bot_message = "(未知)"
|
||||
for entry in reversed(session.mental_log):
|
||||
if entry.event_type == EventType.BOT_PLANNING and entry.actions:
|
||||
for action in entry.actions:
|
||||
if action.get("type") == "kfc_reply":
|
||||
content = action.get("content", "")
|
||||
if content:
|
||||
last_bot_message = content[:100] + ("..." if len(content) > 100 else "")
|
||||
break
|
||||
if last_bot_message != "(未知)":
|
||||
break
|
||||
|
||||
# 构建提示词
|
||||
elapsed_minutes = session.waiting_config.get_elapsed_minutes()
|
||||
max_wait_minutes = session.waiting_config.max_wait_seconds / 60
|
||||
expected_reaction = session.waiting_config.expected_reaction or "对方能回复点什么"
|
||||
|
||||
prompt = await global_prompt_manager.format_prompt(
|
||||
PROMPT_NAMES["waiting_thought"],
|
||||
persona_block=persona_block,
|
||||
user_name=user_name,
|
||||
relation_block=relation_block,
|
||||
last_bot_message=last_bot_message,
|
||||
expected_reaction=expected_reaction,
|
||||
elapsed_minutes=elapsed_minutes,
|
||||
max_wait_minutes=max_wait_minutes,
|
||||
progress_percent=int(progress * 100),
|
||||
)
|
||||
|
||||
# 调用情绪模型
|
||||
models = llm_api.get_available_models()
|
||||
emotion_config = models.get("emotion") or models.get("replyer")
|
||||
|
||||
if not emotion_config:
|
||||
logger.warning("[ProactiveThinker] 未找到 emotion/replyer 模型配置,使用默认想法")
|
||||
return self._get_fallback_thought(elapsed_minutes, progress)
|
||||
|
||||
success, raw_response, _, model_name = await llm_api.generate_with_model(
|
||||
prompt=prompt,
|
||||
model_config=emotion_config,
|
||||
request_type="kokoro_flow_chatter.waiting_thought",
|
||||
)
|
||||
|
||||
if not success or not raw_response:
|
||||
logger.warning(f"[ProactiveThinker] LLM 调用失败: {raw_response}")
|
||||
return self._get_fallback_thought(elapsed_minutes, progress)
|
||||
|
||||
# 使用统一的文本清理函数
|
||||
thought = _clean_reply_text(raw_response)
|
||||
|
||||
logger.debug(f"[ProactiveThinker] LLM 生成等待想法 (model={model_name}): {thought[:50]}...")
|
||||
return thought
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[ProactiveThinker] 生成等待想法失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return self._get_fallback_thought(
|
||||
session.waiting_config.get_elapsed_minutes(),
|
||||
progress
|
||||
)
|
||||
|
||||
def _get_fallback_thought(self, elapsed_minutes: float, progress: float) -> str:
|
||||
"""获取备用的等待想法(当 LLM 调用失败时使用)"""
|
||||
if progress < 0.4:
|
||||
thoughts = [
|
||||
f"已经等了 {elapsed_minutes:.0f} 分钟了,对方可能在忙吧...",
|
||||
@@ -253,9 +351,16 @@ class ProactiveThinker:
|
||||
"要不要主动说点什么呢...",
|
||||
"快到时间了,对方还是没回",
|
||||
]
|
||||
|
||||
return random.choice(thoughts)
|
||||
|
||||
async def _get_person_info_manager(self):
|
||||
"""获取 person_info_manager"""
|
||||
try:
|
||||
from src.person_info.person_info import get_person_info_manager
|
||||
return get_person_info_manager()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
async def _handle_timeout(self, session: KokoroSession) -> None:
|
||||
"""处理等待超时"""
|
||||
self._stats["timeout_decisions"] += 1
|
||||
@@ -276,6 +381,9 @@ class ProactiveThinker:
|
||||
logger.info(f"[ProactiveThinker] 等待超时: user={session.user_id}")
|
||||
|
||||
try:
|
||||
# 获取用户名
|
||||
user_name = await self._get_user_name(session.user_id, session.stream_id)
|
||||
|
||||
# 获取聊天流
|
||||
chat_stream = await self._get_chat_stream(session.stream_id)
|
||||
|
||||
@@ -288,41 +396,59 @@ class ProactiveThinker:
|
||||
action_modifier = ActionModifier(action_manager, session.stream_id)
|
||||
await action_modifier.modify_actions(chatter_name="KokoroFlowChatter")
|
||||
|
||||
# 调用 Replyer 生成超时决策
|
||||
response = await generate_response(
|
||||
# 调用 Planner 生成超时决策
|
||||
plan_response = await generate_plan(
|
||||
session=session,
|
||||
user_name=session.user_id, # 这里可以改进,获取真实用户名
|
||||
user_name=user_name,
|
||||
situation_type="timeout",
|
||||
chat_stream=chat_stream,
|
||||
available_actions=action_manager.get_using_actions(),
|
||||
)
|
||||
|
||||
# 对于需要回复的动作,调用 Replyer 生成实际文本
|
||||
processed_actions = []
|
||||
for action in plan_response.actions:
|
||||
if action.type == "kfc_reply":
|
||||
success, reply_text = await generate_reply_text(
|
||||
session=session,
|
||||
user_name=user_name,
|
||||
thought=plan_response.thought,
|
||||
situation_type="timeout",
|
||||
chat_stream=chat_stream,
|
||||
)
|
||||
if success and reply_text:
|
||||
action.params["content"] = reply_text
|
||||
else:
|
||||
logger.warning("[ProactiveThinker] 回复生成失败,跳过该动作")
|
||||
continue
|
||||
processed_actions.append(action)
|
||||
|
||||
# 执行动作
|
||||
for action in response.actions:
|
||||
for action in processed_actions:
|
||||
await action_manager.execute_action(
|
||||
action_name=action.type,
|
||||
chat_id=session.stream_id,
|
||||
target_message=None,
|
||||
reasoning=response.thought,
|
||||
reasoning=plan_response.thought,
|
||||
action_data=action.params,
|
||||
thinking_id=None,
|
||||
log_prefix="[KFC V2 ProactiveThinker]",
|
||||
log_prefix="[KFC ProactiveThinker]",
|
||||
)
|
||||
|
||||
# 记录到 mental_log
|
||||
session.add_bot_planning(
|
||||
thought=response.thought,
|
||||
actions=[a.to_dict() for a in response.actions],
|
||||
expected_reaction=response.expected_reaction,
|
||||
max_wait_seconds=response.max_wait_seconds,
|
||||
thought=plan_response.thought,
|
||||
actions=[a.to_dict() for a in processed_actions],
|
||||
expected_reaction=plan_response.expected_reaction,
|
||||
max_wait_seconds=plan_response.max_wait_seconds,
|
||||
)
|
||||
|
||||
# 更新状态
|
||||
if response.max_wait_seconds > 0:
|
||||
if plan_response.max_wait_seconds > 0:
|
||||
# 继续等待
|
||||
session.start_waiting(
|
||||
expected_reaction=response.expected_reaction,
|
||||
max_wait_seconds=response.max_wait_seconds,
|
||||
expected_reaction=plan_response.expected_reaction,
|
||||
max_wait_seconds=plan_response.max_wait_seconds,
|
||||
)
|
||||
else:
|
||||
# 不再等待
|
||||
@@ -333,8 +459,8 @@ class ProactiveThinker:
|
||||
|
||||
logger.info(
|
||||
f"[ProactiveThinker] 超时决策完成: user={session.user_id}, "
|
||||
f"actions={[a.type for a in response.actions]}, "
|
||||
f"continue_wait={response.max_wait_seconds > 0}"
|
||||
f"actions={[a.type for a in processed_actions]}, "
|
||||
f"continue_wait={plan_response.max_wait_seconds > 0}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -430,6 +556,9 @@ class ProactiveThinker:
|
||||
logger.info(f"[ProactiveThinker] 主动思考触发: user={session.user_id}, reason={trigger_reason}")
|
||||
|
||||
try:
|
||||
# 获取用户名
|
||||
user_name = await self._get_user_name(session.user_id, session.stream_id)
|
||||
|
||||
# 获取聊天流
|
||||
chat_stream = await self._get_chat_stream(session.stream_id)
|
||||
|
||||
@@ -449,23 +578,25 @@ class ProactiveThinker:
|
||||
else:
|
||||
silence_duration = f"{silence_seconds / 3600:.1f} 小时"
|
||||
|
||||
# 调用 Replyer
|
||||
response = await generate_response(
|
||||
extra_context = {
|
||||
"trigger_reason": trigger_reason,
|
||||
"silence_duration": silence_duration,
|
||||
}
|
||||
|
||||
# 调用 Planner
|
||||
plan_response = await generate_plan(
|
||||
session=session,
|
||||
user_name=session.user_id,
|
||||
user_name=user_name,
|
||||
situation_type="proactive",
|
||||
chat_stream=chat_stream,
|
||||
available_actions=action_manager.get_using_actions(),
|
||||
extra_context={
|
||||
"trigger_reason": trigger_reason,
|
||||
"silence_duration": silence_duration,
|
||||
},
|
||||
extra_context=extra_context,
|
||||
)
|
||||
|
||||
# 检查是否决定不打扰
|
||||
is_do_nothing = (
|
||||
len(response.actions) == 0 or
|
||||
(len(response.actions) == 1 and response.actions[0].type == "do_nothing")
|
||||
len(plan_response.actions) == 0 or
|
||||
(len(plan_response.actions) == 1 and plan_response.actions[0].type == "do_nothing")
|
||||
)
|
||||
|
||||
if is_do_nothing:
|
||||
@@ -474,32 +605,51 @@ class ProactiveThinker:
|
||||
await self.session_manager.save_session(session.user_id)
|
||||
return
|
||||
|
||||
# 对于需要回复的动作,调用 Replyer 生成实际文本
|
||||
processed_actions = []
|
||||
for action in plan_response.actions:
|
||||
if action.type == "kfc_reply":
|
||||
success, reply_text = await generate_reply_text(
|
||||
session=session,
|
||||
user_name=user_name,
|
||||
thought=plan_response.thought,
|
||||
situation_type="proactive",
|
||||
chat_stream=chat_stream,
|
||||
extra_context=extra_context,
|
||||
)
|
||||
if success and reply_text:
|
||||
action.params["content"] = reply_text
|
||||
else:
|
||||
logger.warning("[ProactiveThinker] 回复生成失败,跳过该动作")
|
||||
continue
|
||||
processed_actions.append(action)
|
||||
|
||||
# 执行动作
|
||||
for action in response.actions:
|
||||
for action in processed_actions:
|
||||
await action_manager.execute_action(
|
||||
action_name=action.type,
|
||||
chat_id=session.stream_id,
|
||||
target_message=None,
|
||||
reasoning=response.thought,
|
||||
reasoning=plan_response.thought,
|
||||
action_data=action.params,
|
||||
thinking_id=None,
|
||||
log_prefix="[KFC V2 ProactiveThinker]",
|
||||
log_prefix="[KFC ProactiveThinker]",
|
||||
)
|
||||
|
||||
# 记录到 mental_log
|
||||
session.add_bot_planning(
|
||||
thought=response.thought,
|
||||
actions=[a.to_dict() for a in response.actions],
|
||||
expected_reaction=response.expected_reaction,
|
||||
max_wait_seconds=response.max_wait_seconds,
|
||||
thought=plan_response.thought,
|
||||
actions=[a.to_dict() for a in processed_actions],
|
||||
expected_reaction=plan_response.expected_reaction,
|
||||
max_wait_seconds=plan_response.max_wait_seconds,
|
||||
)
|
||||
|
||||
# 更新状态
|
||||
session.last_proactive_at = time.time()
|
||||
if response.max_wait_seconds > 0:
|
||||
if plan_response.max_wait_seconds > 0:
|
||||
session.start_waiting(
|
||||
expected_reaction=response.expected_reaction,
|
||||
max_wait_seconds=response.max_wait_seconds,
|
||||
expected_reaction=plan_response.expected_reaction,
|
||||
max_wait_seconds=plan_response.max_wait_seconds,
|
||||
)
|
||||
|
||||
# 保存
|
||||
@@ -507,7 +657,7 @@ class ProactiveThinker:
|
||||
|
||||
logger.info(
|
||||
f"[ProactiveThinker] 主动发起完成: user={session.user_id}, "
|
||||
f"actions={[a.type for a in response.actions]}"
|
||||
f"actions={[a.type for a in processed_actions]}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -525,6 +675,25 @@ class ProactiveThinker:
|
||||
logger.warning(f"[ProactiveThinker] 获取 chat_stream 失败: {e}")
|
||||
return None
|
||||
|
||||
async def _get_user_name(self, user_id: str, stream_id: str) -> str:
|
||||
"""获取用户名称(优先从 person_info 获取)"""
|
||||
try:
|
||||
from src.person_info.person_info import get_person_info_manager
|
||||
|
||||
person_info_manager = get_person_info_manager()
|
||||
platform = global_config.bot.platform if global_config else "qq"
|
||||
|
||||
person_id = person_info_manager.get_person_id(platform, user_id)
|
||||
person_name = await person_info_manager.get_value(person_id, "person_name")
|
||||
|
||||
if person_name:
|
||||
return person_name
|
||||
except Exception as e:
|
||||
logger.debug(f"[ProactiveThinker] 获取用户名失败: {e}")
|
||||
|
||||
# 回退到 user_id
|
||||
return user_id
|
||||
|
||||
def get_stats(self) -> dict:
|
||||
"""获取统计信息"""
|
||||
return {
|
||||
|
||||
@@ -38,7 +38,7 @@ class PromptBuilder:
|
||||
def __init__(self):
|
||||
self._context_builder = None
|
||||
|
||||
async def build_prompt(
|
||||
async def build_planner_prompt(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
user_name: str,
|
||||
@@ -48,7 +48,7 @@ class PromptBuilder:
|
||||
extra_context: Optional[dict] = None,
|
||||
) -> str:
|
||||
"""
|
||||
构建完整的提示词
|
||||
构建规划器提示词(用于生成行动计划)
|
||||
|
||||
Args:
|
||||
session: 会话对象
|
||||
@@ -59,7 +59,7 @@ class PromptBuilder:
|
||||
extra_context: 额外上下文(如 trigger_reason)
|
||||
|
||||
Returns:
|
||||
完整的提示词
|
||||
完整的规划器提示词
|
||||
"""
|
||||
extra_context = extra_context or {}
|
||||
|
||||
@@ -89,8 +89,8 @@ class PromptBuilder:
|
||||
# 6. 构建可用动作
|
||||
actions_block = self._build_actions_block(available_actions)
|
||||
|
||||
# 7. 获取输出格式
|
||||
output_format = await self._get_output_format()
|
||||
# 7. 获取规划器输出格式
|
||||
output_format = await self._get_planner_output_format()
|
||||
|
||||
# 8. 使用统一的 prompt 管理系统格式化
|
||||
prompt = await global_prompt_manager.format_prompt(
|
||||
@@ -109,6 +109,76 @@ class PromptBuilder:
|
||||
|
||||
return prompt
|
||||
|
||||
async def build_replyer_prompt(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
user_name: str,
|
||||
thought: str,
|
||||
situation_type: str = "new_message",
|
||||
chat_stream: Optional["ChatStream"] = None,
|
||||
extra_context: Optional[dict] = None,
|
||||
) -> str:
|
||||
"""
|
||||
构建回复器提示词(用于生成自然的回复文本)
|
||||
|
||||
Args:
|
||||
session: 会话对象
|
||||
user_name: 用户名称
|
||||
thought: 规划器生成的想法
|
||||
situation_type: 情况类型
|
||||
chat_stream: 聊天流对象
|
||||
extra_context: 额外上下文
|
||||
|
||||
Returns:
|
||||
完整的回复器提示词
|
||||
"""
|
||||
extra_context = extra_context or {}
|
||||
|
||||
# 获取 user_id
|
||||
user_id = session.user_id if session else None
|
||||
|
||||
# 1. 构建人设块
|
||||
persona_block = self._build_persona_block()
|
||||
|
||||
# 2. 使用 context_builder 获取关系、记忆、表达习惯等
|
||||
context_data = await self._build_context_data(user_name, chat_stream, user_id)
|
||||
relation_block = context_data.get("relation_info", f"你与 {user_name} 还不太熟悉,这是早期的交流阶段。")
|
||||
memory_block = context_data.get("memory_block", "")
|
||||
expression_habits = self._build_combined_expression_block(context_data.get("expression_habits", ""))
|
||||
|
||||
# 3. 构建活动流
|
||||
activity_stream = await self._build_activity_stream(session, user_name)
|
||||
|
||||
# 4. 构建当前情况(回复器专用,简化版,不包含决策语言)
|
||||
current_situation = await self._build_replyer_situation(
|
||||
session, user_name, situation_type, extra_context
|
||||
)
|
||||
|
||||
# 5. 构建聊天历史总览
|
||||
chat_history_block = await self._build_chat_history_block(chat_stream)
|
||||
|
||||
# 6. 构建回复情景上下文
|
||||
reply_context = await self._build_reply_context(
|
||||
session, user_name, situation_type, extra_context
|
||||
)
|
||||
|
||||
# 7. 使用回复器专用模板
|
||||
prompt = await global_prompt_manager.format_prompt(
|
||||
PROMPT_NAMES["replyer"],
|
||||
user_name=user_name,
|
||||
persona_block=persona_block,
|
||||
relation_block=relation_block,
|
||||
memory_block=memory_block or "(暂无相关记忆)",
|
||||
activity_stream=activity_stream or "(这是你们第一次聊天)",
|
||||
current_situation=current_situation,
|
||||
chat_history_block=chat_history_block,
|
||||
expression_habits=expression_habits or "(根据自然对话风格回复即可)",
|
||||
thought=thought,
|
||||
reply_context=reply_context,
|
||||
)
|
||||
|
||||
return prompt
|
||||
|
||||
def _build_persona_block(self) -> str:
|
||||
"""构建人设块"""
|
||||
if global_config is None:
|
||||
@@ -140,7 +210,7 @@ class PromptBuilder:
|
||||
|
||||
# 1. 添加说话风格(来自配置)
|
||||
if global_config and global_config.personality.reply_style:
|
||||
parts.append(f"**说话风格**:\n{global_config.personality.reply_style}")
|
||||
parts.append(f"**说话风格**:\n你必须参考你的说话风格:\n{global_config.personality.reply_style}")
|
||||
|
||||
# 2. 添加学习到的表达习惯
|
||||
if learned_habits and learned_habits.strip():
|
||||
@@ -245,9 +315,15 @@ class PromptBuilder:
|
||||
if not history_messages:
|
||||
return "(暂无聊天记录)"
|
||||
|
||||
# 过滤非文本消息(如戳一戳、禁言等系统通知)
|
||||
text_messages = self._filter_text_messages(history_messages)
|
||||
|
||||
if not text_messages:
|
||||
return "(暂无聊天记录)"
|
||||
|
||||
# 构建可读消息
|
||||
chat_content, _ = await build_readable_messages_with_id(
|
||||
messages=[msg.flatten() for msg in history_messages[-30:]], # 最多30条
|
||||
messages=[msg.flatten() for msg in text_messages[-30:]], # 最多30条
|
||||
timestamp_mode="normal_no_YMD",
|
||||
truncate=False,
|
||||
show_actions=False,
|
||||
@@ -259,6 +335,33 @@ class PromptBuilder:
|
||||
logger.warning(f"构建聊天历史块失败: {e}")
|
||||
return "(获取聊天记录失败)"
|
||||
|
||||
def _filter_text_messages(self, messages: list) -> list:
|
||||
"""
|
||||
过滤非文本消息
|
||||
|
||||
移除系统通知消息(如戳一戳、禁言等),只保留正常的文本聊天消息
|
||||
|
||||
Args:
|
||||
messages: 消息列表(DatabaseMessages 对象)
|
||||
|
||||
Returns:
|
||||
过滤后的消息列表
|
||||
"""
|
||||
filtered = []
|
||||
for msg in messages:
|
||||
# 跳过系统通知消息(戳一戳、禁言等)
|
||||
if getattr(msg, "is_notify", False):
|
||||
continue
|
||||
|
||||
# 跳过没有实际文本内容的消息
|
||||
content = getattr(msg, "processed_plain_text", "") or getattr(msg, "display_message", "")
|
||||
if not content or not content.strip():
|
||||
continue
|
||||
|
||||
filtered.append(msg)
|
||||
|
||||
return filtered
|
||||
|
||||
async def _build_activity_stream(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
@@ -578,6 +681,151 @@ class PromptBuilder:
|
||||
"expected_reaction": "期待的反应",
|
||||
"max_wait_seconds": 300
|
||||
}"""
|
||||
|
||||
async def _get_planner_output_format(self) -> str:
|
||||
"""获取规划器输出格式模板"""
|
||||
try:
|
||||
prompt = await global_prompt_manager.get_prompt_async(
|
||||
PROMPT_NAMES["planner_output_format"]
|
||||
)
|
||||
return prompt.template
|
||||
except KeyError:
|
||||
# 如果模板未注册,返回默认格式
|
||||
return """请用 JSON 格式回复:
|
||||
{
|
||||
"thought": "你的想法",
|
||||
"actions": [{"type": "kfc_reply"}],
|
||||
"expected_reaction": "期待的反应",
|
||||
"max_wait_seconds": 300
|
||||
}
|
||||
|
||||
注意:kfc_reply 动作不需要填写 content 字段,回复内容会单独生成。"""
|
||||
|
||||
async def _build_replyer_situation(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
user_name: str,
|
||||
situation_type: str,
|
||||
extra_context: dict,
|
||||
) -> str:
|
||||
"""
|
||||
构建回复器专用的当前情况描述
|
||||
|
||||
与 Planner 的 _build_current_situation 不同,这里不包含决策性语言,
|
||||
只描述当前的情景背景
|
||||
"""
|
||||
from datetime import datetime
|
||||
current_time = datetime.now().strftime("%Y年%m月%d日 %H:%M")
|
||||
|
||||
if situation_type == "new_message":
|
||||
return f"现在是 {current_time}。{user_name} 刚给你发了消息。"
|
||||
|
||||
elif situation_type == "reply_in_time":
|
||||
elapsed = session.waiting_config.get_elapsed_seconds()
|
||||
max_wait = session.waiting_config.max_wait_seconds
|
||||
return (
|
||||
f"现在是 {current_time}。\n"
|
||||
f"你之前发了消息后在等 {user_name} 的回复。"
|
||||
f"等了大约 {elapsed / 60:.1f} 分钟(你原本打算最多等 {max_wait / 60:.1f} 分钟)。"
|
||||
f"现在 {user_name} 回复了!"
|
||||
)
|
||||
|
||||
elif situation_type == "reply_late":
|
||||
elapsed = session.waiting_config.get_elapsed_seconds()
|
||||
max_wait = session.waiting_config.max_wait_seconds
|
||||
return (
|
||||
f"现在是 {current_time}。\n"
|
||||
f"你之前发了消息后在等 {user_name} 的回复。"
|
||||
f"你原本打算最多等 {max_wait / 60:.1f} 分钟,但实际等了 {elapsed / 60:.1f} 分钟才收到回复。"
|
||||
f"虽然有点迟,但 {user_name} 终于回复了。"
|
||||
)
|
||||
|
||||
elif situation_type == "timeout":
|
||||
elapsed = session.waiting_config.get_elapsed_seconds()
|
||||
max_wait = session.waiting_config.max_wait_seconds
|
||||
return (
|
||||
f"现在是 {current_time}。\n"
|
||||
f"你之前发了消息后一直在等 {user_name} 的回复。"
|
||||
f"你原本打算最多等 {max_wait / 60:.1f} 分钟,现在已经等了 {elapsed / 60:.1f} 分钟了,对方还是没回。"
|
||||
f"你决定主动说点什么。"
|
||||
)
|
||||
|
||||
elif situation_type == "proactive":
|
||||
silence = extra_context.get("silence_duration", "一段时间")
|
||||
return (
|
||||
f"现在是 {current_time}。\n"
|
||||
f"你和 {user_name} 已经有一段时间没聊天了(沉默了 {silence})。"
|
||||
f"你决定主动找 {user_name} 聊点什么。"
|
||||
)
|
||||
|
||||
# 默认
|
||||
return f"现在是 {current_time}。"
|
||||
|
||||
async def _build_reply_context(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
user_name: str,
|
||||
situation_type: str,
|
||||
extra_context: dict,
|
||||
) -> str:
|
||||
"""
|
||||
构建回复情景上下文
|
||||
|
||||
根据 situation_type 构建不同的情景描述,帮助回复器理解当前要回复的情境
|
||||
"""
|
||||
# 获取最后一条用户消息
|
||||
target_message = ""
|
||||
entries = session.get_recent_entries(limit=10)
|
||||
for entry in reversed(entries):
|
||||
if entry.event_type == EventType.USER_MESSAGE:
|
||||
target_message = entry.content or ""
|
||||
break
|
||||
|
||||
if situation_type == "new_message":
|
||||
return await global_prompt_manager.format_prompt(
|
||||
PROMPT_NAMES["replyer_context_normal"],
|
||||
user_name=user_name,
|
||||
target_message=target_message or "(无消息内容)",
|
||||
)
|
||||
|
||||
elif situation_type == "reply_in_time":
|
||||
elapsed = session.waiting_config.get_elapsed_seconds()
|
||||
max_wait = session.waiting_config.max_wait_seconds
|
||||
return await global_prompt_manager.format_prompt(
|
||||
PROMPT_NAMES["replyer_context_in_time"],
|
||||
user_name=user_name,
|
||||
target_message=target_message or "(无消息内容)",
|
||||
elapsed_minutes=elapsed / 60,
|
||||
max_wait_minutes=max_wait / 60,
|
||||
)
|
||||
|
||||
elif situation_type == "reply_late":
|
||||
elapsed = session.waiting_config.get_elapsed_seconds()
|
||||
max_wait = session.waiting_config.max_wait_seconds
|
||||
return await global_prompt_manager.format_prompt(
|
||||
PROMPT_NAMES["replyer_context_late"],
|
||||
user_name=user_name,
|
||||
target_message=target_message or "(无消息内容)",
|
||||
elapsed_minutes=elapsed / 60,
|
||||
max_wait_minutes=max_wait / 60,
|
||||
)
|
||||
|
||||
elif situation_type == "proactive":
|
||||
silence = extra_context.get("silence_duration", "一段时间")
|
||||
trigger_reason = extra_context.get("trigger_reason", "")
|
||||
return await global_prompt_manager.format_prompt(
|
||||
PROMPT_NAMES["replyer_context_proactive"],
|
||||
user_name=user_name,
|
||||
silence_duration=silence,
|
||||
trigger_reason=trigger_reason,
|
||||
)
|
||||
|
||||
# 默认使用普通情景
|
||||
return await global_prompt_manager.format_prompt(
|
||||
PROMPT_NAMES["replyer_context_normal"],
|
||||
user_name=user_name,
|
||||
target_message=target_message or "(无消息内容)",
|
||||
)
|
||||
|
||||
|
||||
# 全局单例
|
||||
|
||||
@@ -212,10 +212,181 @@ kfc_ENTRY_PROACTIVE_TRIGGER = Prompt(
|
||||
""",
|
||||
)
|
||||
|
||||
# =================================================================================================
|
||||
# Planner 专用输出格式
|
||||
# =================================================================================================
|
||||
|
||||
kfc_PLANNER_OUTPUT_FORMAT = Prompt(
|
||||
name="kfc_planner_output_format",
|
||||
template="""请用以下 JSON 格式回复:
|
||||
```json
|
||||
{{
|
||||
"thought": "你脑子里在想什么,越自然越好",
|
||||
"actions": [
|
||||
{{"type": "动作名称", ...动作参数}}
|
||||
],
|
||||
"expected_reaction": "你期待对方的反应是什么",
|
||||
"max_wait_seconds": 300
|
||||
}}
|
||||
```
|
||||
|
||||
### 字段说明
|
||||
- `thought`:你的内心独白,记录你此刻的想法和感受。要自然,不要技术性语言。
|
||||
- `actions`:你要执行的动作列表。每个动作是一个对象,必须包含 `type` 字段指定动作类型,其他字段根据动作类型不同而不同(参考上面每个动作的示例)。
|
||||
- 对于 `kfc_reply` 动作,只需要指定 `{{"type": "kfc_reply"}}`,不需要填写 `content` 字段(回复内容会单独生成)
|
||||
- `expected_reaction`:你期待对方如何回应(用于判断是否需要等待)
|
||||
- `max_wait_seconds`:设定等待时间(秒),0 表示不等待,超时后你会考虑是否要主动说点什么
|
||||
|
||||
### 注意事项
|
||||
- 动作参数直接写在动作对象里,不需要 `action_data` 包装
|
||||
- 即使什么都不想做,也放一个 `{{"type": "do_nothing"}}`
|
||||
- 可以组合多个动作,比如先发消息再发表情""",
|
||||
)
|
||||
|
||||
# =================================================================================================
|
||||
# Replyer 专用提示词模板
|
||||
# =================================================================================================
|
||||
|
||||
kfc_REPLYER_PROMPT = Prompt(
|
||||
name="kfc_replyer",
|
||||
template="""# 你与 {user_name} 的私聊
|
||||
|
||||
## 1. 你是谁
|
||||
{persona_block}
|
||||
|
||||
## 2. 你与 {user_name} 的关系
|
||||
{relation_block}
|
||||
|
||||
## 3. 相关记忆
|
||||
{memory_block}
|
||||
|
||||
## 4. 你们之间发生的事(活动流)
|
||||
以下是你和 {user_name} 最近的互动历史,按时间顺序记录了你们的对话和你的心理活动:
|
||||
|
||||
{activity_stream}
|
||||
|
||||
## 5. 当前情况
|
||||
{current_situation}
|
||||
|
||||
## 6. 聊天历史总览
|
||||
以下是你和 {user_name} 的聊天记录,帮助你更好地理解对话上下文:
|
||||
|
||||
{chat_history_block}
|
||||
|
||||
## 7. 你的表达习惯
|
||||
{expression_habits}
|
||||
|
||||
## 8. 你的决策
|
||||
你已经决定要回复 {user_name}。
|
||||
|
||||
**你的想法**:{thought}
|
||||
|
||||
{reply_context}
|
||||
|
||||
## 9. 回复要求
|
||||
|
||||
**情景化表达**:
|
||||
- 根据你们的关系亲密度选择合适的语气和称呼
|
||||
- 参考活动流中的互动历史,保持对话的连贯性
|
||||
- 如果对方回复得晚/早,可以自然地表现出你的感受
|
||||
|
||||
**自然对话**:
|
||||
- 像真实的朋友聊天一样,不要生硬或公式化
|
||||
- 可以用口语、网络用语、语气词,让回复更生动
|
||||
- 长度适中,不要太长也不要太短
|
||||
|
||||
**表达习惯**:
|
||||
- 参考上面的"表达习惯"部分,使用你习惯的语言风格
|
||||
- 保持人设的一致性
|
||||
|
||||
**禁忌**:
|
||||
- 不要重复你之前说过的话
|
||||
- 不要输出 JSON 格式或技术性语言
|
||||
- 不要加引号、括号等多余符号
|
||||
- 不要用"我决定..."、"因此..."这种总结性语言
|
||||
|
||||
现在,请直接输出你要说的话:""",
|
||||
)
|
||||
|
||||
kfc_REPLYER_CONTEXT_NORMAL = Prompt(
|
||||
name="kfc_replyer_context_normal",
|
||||
template="""你要回复的是 {user_name} 刚发来的消息:
|
||||
「{target_message}」""",
|
||||
)
|
||||
|
||||
kfc_REPLYER_CONTEXT_IN_TIME = Prompt(
|
||||
name="kfc_replyer_context_in_time",
|
||||
template="""你等了 {elapsed_minutes:.1f} 分钟(原本打算最多等 {max_wait_minutes:.1f} 分钟),{user_name} 终于回复了:
|
||||
「{target_message}」
|
||||
|
||||
你可以表现出一点"等到了回复"的欣喜或轻松。""",
|
||||
)
|
||||
|
||||
kfc_REPLYER_CONTEXT_LATE = Prompt(
|
||||
name="kfc_replyer_context_late",
|
||||
template="""你等了 {elapsed_minutes:.1f} 分钟(原本只打算等 {max_wait_minutes:.1f} 分钟),{user_name} 才回复:
|
||||
「{target_message}」
|
||||
|
||||
虽然有点晚,但对方终于回复了。你可以选择轻轻抱怨一下,也可以装作没在意。""",
|
||||
)
|
||||
|
||||
kfc_REPLYER_CONTEXT_PROACTIVE = Prompt(
|
||||
name="kfc_replyer_context_proactive",
|
||||
template="""你们已经有一段时间({silence_duration})没聊天了。{trigger_reason}
|
||||
|
||||
你决定主动打破沉默,找 {user_name} 聊点什么。想一个自然的开场白,不要太突兀。""",
|
||||
)
|
||||
|
||||
# =================================================================================================
|
||||
# 等待思考提示词模板(用于生成等待中的心理活动)
|
||||
# =================================================================================================
|
||||
|
||||
kfc_WAITING_THOUGHT = Prompt(
|
||||
name="kfc_waiting_thought",
|
||||
template="""# 等待中的心理活动
|
||||
|
||||
## 你是谁
|
||||
{persona_block}
|
||||
|
||||
## 你与 {user_name} 的关系
|
||||
{relation_block}
|
||||
|
||||
## 当前情况
|
||||
你刚才给 {user_name} 发了消息,现在正在等待对方回复。
|
||||
|
||||
**你发的消息**:{last_bot_message}
|
||||
**你期待的反应**:{expected_reaction}
|
||||
**已等待时间**:{elapsed_minutes:.1f} 分钟
|
||||
**计划最多等待**:{max_wait_minutes:.1f} 分钟
|
||||
**等待进度**:{progress_percent}%
|
||||
|
||||
## 任务
|
||||
请描述你此刻等待时的内心想法。这是你私下的心理活动,不是要发送的消息。
|
||||
|
||||
**要求**:
|
||||
- 用第一人称描述你的感受和想法
|
||||
- 要符合你的性格和你们的关系
|
||||
- 根据等待进度自然表达情绪变化:
|
||||
- 初期(0-40%):可能比较平静,稍微期待
|
||||
- 中期(40-70%):可能开始有点在意,但还好
|
||||
- 后期(70-100%):可能有点焦虑、担心,或者想主动做点什么
|
||||
- 不要太长,1-2句话即可
|
||||
- 不要输出 JSON,直接输出你的想法
|
||||
|
||||
现在,请直接输出你等待时的内心想法:""",
|
||||
)
|
||||
|
||||
# 导出所有模板名称,方便外部引用
|
||||
PROMPT_NAMES = {
|
||||
"main": "kfc_main",
|
||||
"output_format": "kfc_output_format",
|
||||
"planner_output_format": "kfc_planner_output_format",
|
||||
"replyer": "kfc_replyer",
|
||||
"replyer_context_normal": "kfc_replyer_context_normal",
|
||||
"replyer_context_in_time": "kfc_replyer_context_in_time",
|
||||
"replyer_context_late": "kfc_replyer_context_late",
|
||||
"replyer_context_proactive": "kfc_replyer_context_proactive",
|
||||
"waiting_thought": "kfc_waiting_thought",
|
||||
"situation_new_message": "kfc_situation_new_message",
|
||||
"situation_reply_in_time": "kfc_situation_reply_in_time",
|
||||
"situation_reply_late": "kfc_situation_reply_late",
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
"""
|
||||
Kokoro Flow Chatter - Replyer
|
||||
|
||||
简化的回复生成模块,使用插件系统的 llm_api
|
||||
纯粹的回复生成器:
|
||||
- 接收 planner 的决策(thought 等)
|
||||
- 专门负责将回复意图转化为自然的对话文本
|
||||
- 不输出 JSON,直接生成可发送的消息文本
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.plugin_system.apis import llm_api
|
||||
from src.utils.json_parser import extract_and_parse_json
|
||||
|
||||
from .models import LLMResponse
|
||||
from .prompt.builder import get_prompt_builder
|
||||
from .session import KokoroSession
|
||||
|
||||
@@ -20,90 +21,103 @@ if TYPE_CHECKING:
|
||||
logger = get_logger("kfc_replyer")
|
||||
|
||||
|
||||
async def generate_response(
|
||||
async def generate_reply_text(
|
||||
session: KokoroSession,
|
||||
user_name: str,
|
||||
thought: str,
|
||||
situation_type: str = "new_message",
|
||||
chat_stream: Optional["ChatStream"] = None,
|
||||
available_actions: Optional[dict] = None,
|
||||
extra_context: Optional[dict] = None,
|
||||
) -> LLMResponse:
|
||||
) -> tuple[bool, str]:
|
||||
"""
|
||||
生成回复
|
||||
生成回复文本
|
||||
|
||||
Args:
|
||||
session: 会话对象
|
||||
user_name: 用户名称
|
||||
thought: 规划器生成的想法(内心独白)
|
||||
situation_type: 情况类型
|
||||
chat_stream: 聊天流对象
|
||||
available_actions: 可用动作字典
|
||||
extra_context: 额外上下文
|
||||
|
||||
Returns:
|
||||
LLMResponse 对象
|
||||
(success, reply_text) 元组
|
||||
- success: 是否成功生成
|
||||
- reply_text: 生成的回复文本
|
||||
"""
|
||||
try:
|
||||
# 1. 构建提示词
|
||||
# 1. 构建回复器提示词
|
||||
prompt_builder = get_prompt_builder()
|
||||
prompt = await prompt_builder.build_prompt(
|
||||
prompt = await prompt_builder.build_replyer_prompt(
|
||||
session=session,
|
||||
user_name=user_name,
|
||||
thought=thought,
|
||||
situation_type=situation_type,
|
||||
chat_stream=chat_stream,
|
||||
available_actions=available_actions,
|
||||
extra_context=extra_context,
|
||||
)
|
||||
|
||||
from src.config.config import global_config
|
||||
if global_config and global_config.debug.show_prompt:
|
||||
logger.info(f"[KFC Replyer] 生成的提示词:\n{prompt}")
|
||||
logger.info(f"[KFC Replyer] 生成的回复提示词:\n{prompt}")
|
||||
|
||||
# 2. 获取模型配置并调用 LLM
|
||||
# 2. 获取 replyer 模型配置并调用 LLM
|
||||
models = llm_api.get_available_models()
|
||||
replyer_config = models.get("replyer")
|
||||
|
||||
if not replyer_config:
|
||||
logger.error("[KFC Replyer] 未找到 replyer 模型配置")
|
||||
return LLMResponse.create_error_response("未找到 replyer 模型配置")
|
||||
return False, "(回复生成失败:未找到模型配置)"
|
||||
|
||||
success, raw_response, reasoning, model_name = await llm_api.generate_with_model(
|
||||
prompt=prompt,
|
||||
model_config=replyer_config,
|
||||
request_type="kokoro_flow_chatter",
|
||||
request_type="kokoro_flow_chatter.reply",
|
||||
)
|
||||
|
||||
if not success:
|
||||
logger.error(f"[KFC Replyer] LLM 调用失败: {raw_response}")
|
||||
return LLMResponse.create_error_response(raw_response)
|
||||
return False, "(回复生成失败)"
|
||||
|
||||
logger.debug(f"[KFC Replyer] LLM 响应 (model={model_name}):\n{raw_response}")
|
||||
# 3. 清理并返回回复文本
|
||||
reply_text = _clean_reply_text(raw_response)
|
||||
|
||||
# 3. 解析响应
|
||||
return _parse_response(raw_response)
|
||||
logger.info(f"[KFC Replyer] 生成成功 (model={model_name}): {reply_text[:50]}...")
|
||||
|
||||
return True, reply_text
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[KFC Replyer] 生成失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return LLMResponse.create_error_response(str(e))
|
||||
return False, "(回复生成失败)"
|
||||
|
||||
|
||||
def _parse_response(raw_response: str) -> LLMResponse:
|
||||
"""解析 LLM 响应"""
|
||||
data = extract_and_parse_json(raw_response, strict=False)
|
||||
def _clean_reply_text(raw_text: str) -> str:
|
||||
"""
|
||||
清理回复文本
|
||||
|
||||
if not data or not isinstance(data, dict):
|
||||
logger.warning(f"[KFC Replyer] 无法解析 JSON: {raw_response[:200]}...")
|
||||
return LLMResponse.create_error_response("无法解析响应格式")
|
||||
移除可能的前后缀、引号、markdown 标记等
|
||||
"""
|
||||
text = raw_text.strip()
|
||||
|
||||
response = LLMResponse.from_dict(data)
|
||||
# 移除可能的 markdown 代码块标记
|
||||
if text.startswith("```") and text.endswith("```"):
|
||||
lines = text.split("\n")
|
||||
if len(lines) >= 3:
|
||||
# 移除首尾的 ``` 行
|
||||
text = "\n".join(lines[1:-1]).strip()
|
||||
|
||||
if response.thought:
|
||||
logger.info(
|
||||
f"[KFC Replyer] 解析成功: thought={response.thought[:50]}..., "
|
||||
f"actions={[a.type for a in response.actions]}"
|
||||
)
|
||||
else:
|
||||
logger.warning("[KFC Replyer] 响应缺少 thought")
|
||||
# 移除首尾的引号(如果整个文本被引号包裹)
|
||||
if (text.startswith('"') and text.endswith('"')) or \
|
||||
(text.startswith("'") and text.endswith("'")):
|
||||
text = text[1:-1].strip()
|
||||
|
||||
return response
|
||||
# 移除可能的"你说:"、"回复:"等前缀
|
||||
prefixes_to_remove = ["你说:", "你说:", "回复:", "回复:", "我说:", "我说:"]
|
||||
for prefix in prefixes_to_remove:
|
||||
if text.startswith(prefix):
|
||||
text = text[len(prefix):].strip()
|
||||
break
|
||||
|
||||
return text
|
||||
|
||||
@@ -195,7 +195,7 @@ class PokeAction(BaseAction):
|
||||
for i in range(times):
|
||||
logger.info(f"正在向 {display_name} ({user_id}) 发送第 {i + 1}/{times} 次戳一戳...")
|
||||
await self.send_command(
|
||||
"SEND_POKE", args=poke_args, display_message=f"戳了戳 {display_name} ({i + 1}/{times})"
|
||||
"SEND_POKE", args=poke_args
|
||||
)
|
||||
# 添加一个延迟,避免因发送过快导致后续戳一戳失败
|
||||
await asyncio.sleep(1.5)
|
||||
|
||||
Reference in New Issue
Block a user