rebase 清理

This commit is contained in:
Windpicker-owo
2025-11-19 23:45:47 +08:00
parent 829bc9b4bc
commit 40709d95de
60 changed files with 465 additions and 10066 deletions

View File

@@ -3,7 +3,6 @@
"""
from collections import deque
from typing import List, Dict
from src.common.logger import get_logger

View File

@@ -399,13 +399,21 @@ class ExpressionLearner:
# sourcery skip: use-join
"""
学习并存储表达方式
type: "style" or "grammar"
"""
if type == "style":
type_str = "语言风格"
elif type == "grammar":
type_str = "句法特点"
else:
raise ValueError(f"Invalid type: {type}")
# 检查是否允许在此聊天流中学习(在函数最前面检查)
if not self.can_learn_for_chat():
logger.debug(f"聊天流 {self.chat_name} 不允许学习表达,跳过学习")
return []
res = await self.learn_expression(num)
res = await self.learn_expression(type, num)
if res is None:
return []
@@ -421,10 +429,10 @@ class ExpressionLearner:
learnt_expressions_str = ""
for _chat_id, situation, style in learnt_expressions:
learnt_expressions_str += f"{situation}->{style}\n"
logger.info(f"{group_name} 学习到表达风格:\n{learnt_expressions_str}")
logger.info(f"{group_name} 学习到{type_str}:\n{learnt_expressions_str}")
if not learnt_expressions:
logger.info("没有学习到表达风格")
logger.info(f"没有学习到{type_str}")
return []
# 按chat_id分组
@@ -572,10 +580,16 @@ class ExpressionLearner:
"""从指定聊天流学习表达方式
Args:
num: 学习数量
type: "style" or "grammar"
"""
type_str = "语言风格"
prompt = "learn_style_prompt"
if type == "style":
type_str = "语言风格"
prompt = "learn_style_prompt"
elif type == "grammar":
type_str = "句法特点"
prompt = "learn_grammar_prompt"
else:
raise ValueError(f"Invalid type: {type}")
current_time = time.time()
@@ -766,11 +780,9 @@ class ExpressionLearnerManager:
"""
自动将/data/expression/learnt_style 和 learnt_grammar 下所有expressions.json迁移到数据库。
迁移完成后在/data/expression/done.done写入标记文件存在则跳过。
然后检查done.done2如果没有就删除所有grammar表达并创建该标记文件。
"""
base_dir = os.path.join("data", "expression")
done_flag = os.path.join(base_dir, "done.done")
done_flag2 = os.path.join(base_dir, "done.done2")
# 确保基础目录存在
try:
@@ -805,36 +817,27 @@ class ExpressionLearnerManager:
expr_file = os.path.join(type_dir, chat_id, "expressions.json")
if not os.path.exists(expr_file):
continue
try:
async with aiofiles.open(expr_file, encoding="utf-8") as f:
content = await f.read()
expressions = orjson.loads(content)
for chat_id in chat_ids:
expr_file = os.path.join(type_dir, chat_id, "expressions.json")
if not os.path.exists(expr_file):
if not isinstance(expressions, list):
logger.warning(f"表达方式文件格式错误,跳过: {expr_file}")
continue
try:
with open(expr_file, "r", encoding="utf-8") as f:
expressions = json.load(f)
if not isinstance(expressions, list):
logger.warning(f"表达方式文件格式错误,跳过: {expr_file}")
for expr in expressions:
if not isinstance(expr, dict):
continue
for expr in expressions:
if not isinstance(expr, dict):
continue
situation = expr.get("situation")
style_val = expr.get("style")
count = expr.get("count", 1)
last_active_time = expr.get("last_active_time", time.time())
situation = expr.get("situation")
style_val = expr.get("style")
count = expr.get("count", 1)
last_active_time = expr.get("last_active_time", time.time())
if not situation or not style_val:
logger.warning(f"表达方式缺少必要字段,跳过: {expr}")
continue
if not situation or not style_val:
logger.warning(f"表达方式缺少必要字段,跳过: {expr}")
continue
# 查重同chat_id+type+situation+style
async with get_db_session() as session:
@@ -913,40 +916,5 @@ class ExpressionLearnerManager:
except Exception as e:
logger.error(f"迁移老数据创建日期失败: {e}")
def delete_all_grammar_expressions(self) -> int:
"""
检查expression库中所有type为"grammar"的表达并全部删除
Returns:
int: 删除的grammar表达数量
"""
try:
# 查询所有type为"grammar"的表达
grammar_expressions = Expression.select().where(Expression.type == "grammar")
grammar_count = grammar_expressions.count()
if grammar_count == 0:
logger.info("expression库中没有找到grammar类型的表达")
return 0
logger.info(f"找到 {grammar_count} 个grammar类型的表达开始删除...")
# 删除所有grammar类型的表达
deleted_count = 0
for expr in grammar_expressions:
try:
expr.delete_instance()
deleted_count += 1
except Exception as e:
logger.error(f"删除grammar表达失败: {e}")
continue
logger.info(f"成功删除 {deleted_count} 个grammar类型的表达")
return deleted_count
except Exception as e:
logger.error(f"删除grammar表达过程中发生错误: {e}")
return 0
expression_learner_manager = ExpressionLearnerManager()

View File

@@ -32,7 +32,7 @@ def init_prompt():
以下是可选的表达情境:
{all_situations}
请你分析聊天内容的语境、情绪、话题类型,从上述情境中选择最适合当前聊天情境的,最多{max_num}个情境。
请你分析聊天内容的语境、情绪、话题类型,从上述情境中选择最适合当前聊天情境的{min_num}-{max_num}个情境。
考虑因素包括:
1. 聊天的情绪氛围(轻松、严肃、幽默等)
2. 话题类型(日常、技术、游戏、情感等)
@@ -42,7 +42,7 @@ def init_prompt():
请以JSON格式输出只需要输出选中的情境编号
例如:
{{
"selected_situations": [2, 3, 5, 7, 19]
"selected_situations": [2, 3, 5, 7, 19, 22, 25, 38, 39, 45, 48, 64]
}}
请严格按照JSON格式输出不要包含其他内容
@@ -544,24 +544,34 @@ class ExpressionSelector:
# 检查是否允许在此聊天流中使用表达
if not self.can_use_expression_for_chat(chat_id):
logger.debug(f"聊天流 {chat_id} 不允许使用表达,返回空列表")
return [], []
return []
# 1. 获取35个随机表达方式现在按权重抽取
style_exprs, grammar_exprs = await self.get_random_expressions(chat_id, 30, 0.5, 0.5)
# 2. 构建所有表达方式的索引和情境列表
all_expressions: List[Dict[str, Any]] = []
all_situations: List[str] = []
all_expressions = []
all_situations = []
# 添加style表达方式
for expr in style_exprs:
expr = expr.copy()
all_expressions.append(expr)
all_situations.append(f"{len(all_expressions)}.当 {expr['situation']} 时,使用 {expr['style']}")
if isinstance(expr, dict) and "situation" in expr and "style" in expr:
expr_with_type = expr.copy()
expr_with_type["type"] = "style"
all_expressions.append(expr_with_type)
all_situations.append(f"{len(all_expressions)}.{expr['situation']}")
# 添加grammar表达方式
for expr in grammar_exprs:
if isinstance(expr, dict) and "situation" in expr and "style" in expr:
expr_with_type = expr.copy()
expr_with_type["type"] = "grammar"
all_expressions.append(expr_with_type)
all_situations.append(f"{len(all_expressions)}.{expr['situation']}")
if not all_expressions:
logger.warning("没有找到可用的表达方式")
return [], []
return []
all_situations_str = "\n".join(all_situations)
@@ -577,11 +587,14 @@ class ExpressionSelector:
bot_name=global_config.bot.nickname,
chat_observe_info=chat_info,
all_situations=all_situations_str,
min_num=min_num,
max_num=max_num,
target_message=target_message_str,
target_message_extra_block=target_message_extra_block,
)
# print(prompt)
# 4. 调用LLM
try:
# start_time = time.time()
@@ -589,7 +602,7 @@ class ExpressionSelector:
if not content:
logger.warning("LLM返回空结果")
return [], []
return []
# 5. 解析结果
result = repair_json(content)
@@ -599,17 +612,15 @@ class ExpressionSelector:
if not isinstance(result, dict) or "selected_situations" not in result:
logger.error("LLM返回格式错误")
logger.info(f"LLM返回结果: \n{content}")
return [], []
return []
selected_indices = result["selected_situations"]
# 根据索引获取完整的表达方式
valid_expressions: List[Dict[str, Any]] = []
selected_ids = []
valid_expressions = []
for idx in selected_indices:
if isinstance(idx, int) and 1 <= idx <= len(all_expressions):
expression = all_expressions[idx - 1] # 索引从1开始
selected_ids.append(expression["id"])
valid_expressions.append(expression)
# 对选中的所有表达方式一次性更新count数
@@ -617,7 +628,7 @@ class ExpressionSelector:
asyncio.create_task(self.update_expressions_count_batch(valid_expressions, 0.006)) # noqa: RUF006
# logger.info(f"LLM从{len(all_expressions)}个情境中选择了{len(valid_expressions)}个")
return valid_expressions, selected_ids
return valid_expressions
except Exception as e:
logger.error(f"LLM处理表达方式选择时出错: {e}")

View File

@@ -1,152 +0,0 @@
import asyncio
import math
import re
import traceback
from typing import Tuple, TYPE_CHECKING
from src.chat.heart_flow.heartflow import heartflow
from src.chat.memory_system.Hippocampus import hippocampus_manager
from src.chat.message_receive.message import MessageRecv
from src.chat.message_receive.storage import MessageStorage
from src.chat.utils.chat_message_builder import replace_user_references_sync
from src.chat.utils.timer_calculator import Timer
from src.chat.utils.utils import is_mentioned_bot_in_message
from src.common.logger import get_logger
from src.config.config import global_config
from src.mood.mood_manager import mood_manager
from src.person_info.relationship_manager import get_relationship_manager
if TYPE_CHECKING:
from src.chat.heart_flow.sub_heartflow import SubHeartflow
logger = get_logger("chat")
async def _calculate_interest(message: MessageRecv) -> Tuple[float, bool, list[str]]:
"""计算消息的兴趣度
Args:
message: 待处理的消息对象
Returns:
Tuple[float, bool, list[str]]: (兴趣度, 是否被提及, 关键词)
"""
is_mentioned, _ = is_mentioned_bot_in_message(message)
interested_rate = 0.0
with Timer("记忆激活"):
interested_rate, keywords = await hippocampus_manager.get_activate_from_text(
message.processed_plain_text,
max_depth=4,
fast_retrieval=False,
)
message.key_words = keywords
message.key_words_lite = keywords
logger.debug(f"记忆激活率: {interested_rate:.2f}, 关键词: {keywords}")
text_len = len(message.processed_plain_text)
# 根据文本长度分布调整兴趣度,采用分段函数实现更精确的兴趣度计算
# 基于实际分布0-5字符(26.57%), 6-10字符(27.18%), 11-20字符(22.76%), 21-30字符(10.33%), 31+字符(13.86%)
if text_len == 0:
base_interest = 0.01 # 空消息最低兴趣度
elif text_len <= 5:
# 1-5字符线性增长 0.01 -> 0.03
base_interest = 0.01 + (text_len - 1) * (0.03 - 0.01) / 4
elif text_len <= 10:
# 6-10字符线性增长 0.03 -> 0.06
base_interest = 0.03 + (text_len - 5) * (0.06 - 0.03) / 5
elif text_len <= 20:
# 11-20字符线性增长 0.06 -> 0.12
base_interest = 0.06 + (text_len - 10) * (0.12 - 0.06) / 10
elif text_len <= 30:
# 21-30字符线性增长 0.12 -> 0.18
base_interest = 0.12 + (text_len - 20) * (0.18 - 0.12) / 10
elif text_len <= 50:
# 31-50字符线性增长 0.18 -> 0.22
base_interest = 0.18 + (text_len - 30) * (0.22 - 0.18) / 20
elif text_len <= 100:
# 51-100字符线性增长 0.22 -> 0.26
base_interest = 0.22 + (text_len - 50) * (0.26 - 0.22) / 50
else:
# 100+字符:对数增长 0.26 -> 0.3,增长率递减
base_interest = 0.26 + (0.3 - 0.26) * (math.log10(text_len - 99) / math.log10(901)) # 1000-99=901
# 确保在范围内
base_interest = min(max(base_interest, 0.01), 0.3)
interested_rate += base_interest
if is_mentioned:
interest_increase_on_mention = 1
interested_rate += interest_increase_on_mention
return interested_rate, is_mentioned, keywords
class HeartFCMessageReceiver:
"""心流处理器,负责处理接收到的消息并计算兴趣度"""
def __init__(self):
"""初始化心流处理器,创建消息存储实例"""
self.storage = MessageStorage()
async def process_message(self, message: MessageRecv) -> None:
"""处理接收到的原始消息数据
主要流程:
1. 消息解析与初始化
2. 消息缓冲处理
4. 过滤检查
5. 兴趣度计算
6. 关系处理
Args:
message_data: 原始消息字符串
"""
try:
# 1. 消息解析与初始化
userinfo = message.message_info.user_info
chat = message.chat_stream
# 2. 兴趣度计算与更新
interested_rate, is_mentioned, keywords = await _calculate_interest(message)
message.interest_value = interested_rate
message.is_mentioned = is_mentioned
await self.storage.store_message(message, chat)
subheartflow: SubHeartflow = await heartflow.get_or_create_subheartflow(chat.stream_id) # type: ignore
await subheartflow.heart_fc_instance.add_message(message.to_dict())
if global_config.mood.enable_mood:
chat_mood = mood_manager.get_mood_by_chat_id(subheartflow.chat_id)
asyncio.create_task(chat_mood.update_mood_by_message(message, interested_rate))
# 3. 日志记录
mes_name = chat.group_info.group_name if chat.group_info else "私聊"
# 如果消息中包含图片标识,则将 [picid:...] 替换为 [图片]
picid_pattern = r"\[picid:([^\]]+)\]"
processed_plain_text = re.sub(picid_pattern, "[图片]", message.processed_plain_text)
# 应用用户引用格式替换,将回复<aaa:bbb>和@<aaa:bbb>格式转换为可读格式
processed_plain_text = replace_user_references_sync(
processed_plain_text,
message.message_info.platform, # type: ignore
replace_bot_name=True,
)
if keywords:
logger.info(
f"[{mes_name}]{userinfo.user_nickname}:{processed_plain_text}[兴趣度:{interested_rate:.2f}][关键词:{keywords}]"
) # type: ignore
else:
logger.info(
f"[{mes_name}]{userinfo.user_nickname}:{processed_plain_text}[兴趣度:{interested_rate:.2f}]"
) # type: ignore
_ = Person.register_person(platform=message.message_info.platform, user_id=message.message_info.user_info.user_id,nickname=userinfo.user_nickname) # type: ignore
except Exception as e:
logger.error(f"消息处理失败: {e}")
print(traceback.format_exc())

View File

@@ -1,42 +0,0 @@
from rich.traceback import install
from src.common.logger import get_logger
from src.chat.message_receive.chat_stream import get_chat_manager
from src.chat.chat_loop.heartFC_chat import HeartFChatting
from src.chat.utils.utils import get_chat_type_and_target_info
logger = get_logger("sub_heartflow")
install(extra_lines=3)
class SubHeartflow:
def __init__(
self,
subheartflow_id,
):
"""子心流初始化函数
Args:
subheartflow_id: 子心流唯一标识符
"""
# 基础属性,两个值是一样的
self.subheartflow_id = subheartflow_id
self.chat_id = subheartflow_id
self.is_group_chat, self.chat_target_info = (None, None)
self.log_prefix = get_chat_manager().get_stream_name(self.subheartflow_id) or self.subheartflow_id
# focus模式退出冷却时间管理
self.last_focus_exit_time: float = 0 # 上次退出focus模式的时间
# 随便水群 normal_chat 和 认真水群 focus_chat 实例
# CHAT模式激活 随便水群 FOCUS模式激活 认真水群
self.heart_fc_instance: HeartFChatting = HeartFChatting(
chat_id=self.subheartflow_id,
) # 该sub_heartflow的HeartFChatting实例
async def initialize(self):
"""异步初始化方法,创建兴趣流并确定聊天类型"""
self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.chat_id)
await self.heart_fc_instance.start()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -261,8 +261,6 @@ class MessageSending(MessageProcessBase):
self.display_message = display_message
self.interest_value = 0.0
self.selected_expressions = selected_expressions
def build_reply(self):
"""设置回复消息"""

View File

@@ -682,7 +682,6 @@ class MessageStorage:
should_act=should_act,
key_words=key_words,
key_words_lite=key_words_lite,
additional_config=additional_config_json,
)
async with get_db_session() as session:
session.add(new_message)

View File

@@ -184,133 +184,13 @@ class ActionModifier:
def _check_action_associated_types(self, all_actions: dict[str, ActionInfo], chat_context: "StreamContext"):
type_mismatched_actions: list[tuple[str, str]] = []
for action_name, action_info in all_actions.items():
if action_info.associated_types and not self._check_action_output_types(action_info.associated_types, chat_context):
if action_info.associated_types and not chat_context.check_types(action_info.associated_types):
associated_types_str = ", ".join(action_info.associated_types)
reason = f"适配器不支持(需要: {associated_types_str}"
type_mismatched_actions.append((action_name, reason))
logger.debug(f"{self.log_prefix}决定移除动作: {action_name},原因: {reason}")
return type_mismatched_actions
def _check_action_output_types(self, output_types: list[str], chat_context: StreamContext) -> bool:
"""
检查Action的输出类型是否被当前适配器支持
Args:
output_types: Action需要输出的消息类型列表
chat_context: 聊天上下文
Returns:
bool: 如果所有输出类型都支持则返回True
"""
# 获取当前适配器支持的输出类型
adapter_supported_types = self._get_adapter_supported_output_types(chat_context)
# 检查所有需要的输出类型是否都被支持
for output_type in output_types:
if output_type not in adapter_supported_types:
logger.debug(f"适配器不支持输出类型 '{output_type}',支持的类型: {adapter_supported_types}")
return False
return True
def _get_adapter_supported_output_types(self, chat_context: StreamContext) -> list[str]:
"""
获取当前适配器支持的输出类型列表
Args:
chat_context: 聊天上下文
Returns:
list[str]: 支持的输出类型列表
"""
# 检查additional_config是否存在且不为空
additional_config = None
has_additional_config = False
# 先检查 current_message 是否存在
if not chat_context.current_message:
logger.warning(f"{self.log_prefix} [问题] chat_context.current_message 为 None无法获取适配器支持的类型")
return ["text", "emoji"] # 返回基础类型
if hasattr(chat_context.current_message, "additional_config"):
additional_config = chat_context.current_message.additional_config
# 更准确的非空判断
if additional_config is not None:
if isinstance(additional_config, str) and additional_config.strip():
has_additional_config = True
elif isinstance(additional_config, dict):
# 字典存在就可以即使为空也可能有format_info字段
has_additional_config = True
else:
logger.warning(f"{self.log_prefix} [问题] current_message 没有 additional_config 属性")
logger.debug(f"{self.log_prefix} [调试] has_additional_config: {has_additional_config}")
if has_additional_config:
try:
logger.debug(f"{self.log_prefix} [调试] 开始解析 additional_config")
format_info = None
# 处理additional_config可能是字符串或字典的情况
if isinstance(additional_config, str):
# 如果是字符串尝试解析为JSON
try:
config = orjson.loads(additional_config)
format_info = config.get("format_info")
except (orjson.JSONDecodeError, AttributeError, TypeError) as e:
format_info = None
elif isinstance(additional_config, dict):
# 如果是字典直接获取format_info
format_info = additional_config.get("format_info")
# 如果找到了format_info从中提取支持的类型
if format_info:
if "accept_format" in format_info:
accept_format = format_info["accept_format"]
if isinstance(accept_format, str):
accept_format = [accept_format]
elif isinstance(accept_format, list):
pass
else:
accept_format = list(accept_format) if hasattr(accept_format, "__iter__") else []
# 合并基础类型和适配器特定类型
result = list(set(accept_format))
return result
# 备用检查content_format字段
elif "content_format" in format_info:
content_format = format_info["content_format"]
logger.debug(f"{self.log_prefix} [调试] 找到 content_format: {content_format}")
if isinstance(content_format, str):
content_format = [content_format]
elif isinstance(content_format, list):
pass
else:
content_format = list(content_format) if hasattr(content_format, "__iter__") else []
result = list(set(content_format))
return result
else:
logger.warning(f"{self.log_prefix} [问题] additional_config 中没有 format_info 字段")
except Exception as e:
logger.error(f"{self.log_prefix} [问题] 解析适配器格式信息失败: {e}", exc_info=True)
else:
logger.warning(f"{self.log_prefix} [问题] additional_config 不存在或为空")
# 如果无法获取格式信息,返回默认支持的基础类型
default_types = ["text", "emoji"]
logger.warning(
f"{self.log_prefix} [问题] 无法从适配器获取支持的消息类型,使用默认类型: {default_types}"
)
logger.warning(
f"{self.log_prefix} [问题] 这可能导致某些 Action 被错误地过滤。"
f"请检查适配器是否正确设置了 format_info。"
)
return default_types
async def _get_deactivated_actions_by_type(
self,
actions_with_info: dict[str, ActionInfo],

View File

@@ -1,132 +0,0 @@
"""
PlanGenerator: 负责搜集和汇总所有决策所需的信息,生成一个未经筛选的“原始计划” (Plan)。
"""
import time
from typing import Dict
from src.chat.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat
from src.chat.utils.utils import get_chat_type_and_target_info
from src.common.data_models.database_data_model import DatabaseMessages
from src.common.data_models.info_data_model import Plan, TargetPersonInfo
from src.config.config import global_config
from src.plugin_system.base.component_types import ActionActivationType, ActionInfo, ChatMode, ChatType, ComponentType
from src.plugin_system.core.component_registry import component_registry
class PlanGenerator:
"""
PlanGenerator 负责在规划流程的初始阶段收集所有必要信息。
它会汇总以下信息来构建一个“原始”的 Plan 对象,该对象后续会由 PlanFilter 进行筛选:
- 当前聊天信息 (ID, 目标用户)
- 当前可用的动作列表
- 最近的聊天历史记录
Attributes:
chat_id (str): 当前聊天的唯一标识符。
action_manager (ActionManager): 用于获取可用动作列表的管理器。
"""
def __init__(self, chat_id: str):
"""
初始化 PlanGenerator。
Args:
chat_id (str): 当前聊天的 ID。
"""
from src.chat.planner_actions.action_manager import ActionManager
self.chat_id = chat_id
# 注意ActionManager 可能需要根据实际情况初始化
self.action_manager = ActionManager()
async def generate(self, mode: ChatMode) -> Plan:
"""
收集所有信息,生成并返回一个初始的 Plan 对象。
这个 Plan 对象包含了决策所需的所有上下文信息。
Args:
mode (ChatMode): 当前的聊天模式。
Returns:
Plan: 一个填充了初始上下文信息的 Plan 对象。
"""
_is_group_chat, chat_target_info_dict = get_chat_type_and_target_info(self.chat_id)
target_info = None
if chat_target_info_dict:
target_info = TargetPersonInfo(**chat_target_info_dict)
available_actions = self._get_available_actions()
chat_history_raw = get_raw_msg_before_timestamp_with_chat(
chat_id=self.chat_id,
timestamp=time.time(),
limit=int(global_config.chat.max_context_size),
)
chat_history = [DatabaseMessages(**msg) for msg in await chat_history_raw]
plan = Plan(
chat_id=self.chat_id,
mode=mode,
available_actions=available_actions,
chat_history=chat_history,
target_info=target_info,
)
return plan
def _get_available_actions(self) -> Dict[str, "ActionInfo"]:
"""
从 ActionManager 和组件注册表中获取当前所有可用的动作。
它会合并已注册的动作和系统级动作(如 "no_reply"
并以字典形式返回。
Returns:
Dict[str, "ActionInfo"]: 一个字典,键是动作名称,值是 ActionInfo 对象。
"""
current_available_actions_dict = self.action_manager.get_using_actions()
all_registered_actions: Dict[str, ActionInfo] = component_registry.get_components_by_type( # type: ignore
ComponentType.ACTION
)
current_available_actions = {}
for action_name in current_available_actions_dict:
if action_name in all_registered_actions:
current_available_actions[action_name] = all_registered_actions[action_name]
reply_info = ActionInfo(
name="reply",
component_type=ComponentType.ACTION,
description="系统级动作:选择回复消息的决策",
action_parameters={"content": "回复的文本内容", "reply_to_message_id": "要回复的消息ID"},
action_require=[
"你想要闲聊或者随便附和",
"当用户提到你或艾特你时",
"当需要回答用户的问题时",
"当你想参与对话时",
"当用户分享有趣的内容时",
],
activation_type=ActionActivationType.ALWAYS,
activation_keywords=[],
associated_types=["text", "reply"],
plugin_name="SYSTEM",
enabled=True,
parallel_action=False,
mode_enable=ChatMode.ALL,
chat_type_allow=ChatType.ALL,
)
no_reply_info = ActionInfo(
name="no_reply",
component_type=ComponentType.ACTION,
description="系统级动作:选择不回复消息的决策",
action_parameters={},
activation_keywords=[],
plugin_name="SYSTEM",
enabled=True,
parallel_action=False,
)
current_available_actions["no_reply"] = no_reply_info
current_available_actions["reply"] = reply_info
return current_available_actions

View File

@@ -1,188 +0,0 @@
"""
本文件集中管理所有与规划器Planner相关的提示词Prompt模板。
通过将提示词与代码逻辑分离,可以更方便地对模型的行为进行迭代和优化,
而无需修改核心代码。
"""
from src.chat.utils.prompt import Prompt
def init_prompts():
"""
初始化并向 Prompt 注册系统注册所有规划器相关的提示词。
这个函数会在模块加载时自动调用,确保所有提示词在系统启动时都已准备就绪。
"""
# 核心规划器提示词,用于在接收到新消息时决定如何回应。
# 它构建了一个复杂的上下文,包括历史记录、可用动作、角色设定等,
# 并要求模型以 JSON 格式输出一个或多个动作组合。
Prompt(
"""
{mood_block}
{time_block}
{identity_block}
{users_in_chat}
{custom_prompt_block}
{chat_context_description},以下是具体的聊天内容。
## 📜 已读历史消息(仅供参考)
{read_history_block}
## 📬 未读历史消息(动作执行对象)
{unread_history_block}
{moderation_prompt}
**任务: 构建一个完整的响应**
你的任务是根据当前的聊天内容,构建一个完整的、人性化的响应。一个完整的响应由两部分组成:
1. **主要动作**: 这是响应的核心,通常是 `reply`(如果有)。
2. **辅助动作 (可选)**: 这是为了增强表达效果的附加动作,例如 `emoji`(发送表情包)或 `poke_user`(戳一戳)。
**决策流程:**
1. **重要:已读历史消息仅作为当前聊天情景的参考,帮助你理解对话上下文。**
2. **重要:所有动作的执行对象只能是未读历史消息中的消息,不能对已读消息执行动作。**
3. 在未读历史消息中,优先对兴趣值高的消息做出动作(兴趣值标注在消息末尾)。
4. 首先,决定是否要对未读消息进行 `reply`(如果有)。
5. 然后,评估当前的对话气氛和用户情绪,判断是否需要一个**辅助动作**来让你的回应更生动、更符合你的性格。
6. 如果需要,选择一个最合适的辅助动作与 `reply`(如果有) 组合。
7. 如果用户明确要求了某个动作,请务必优先满足。
**如果可选动作中没有reply请不要使用**
**可用动作:**
{actions_before_now_block}
{no_action_block}
{action_options_text}
**输出格式:**
你必须以严格的 JSON 格式输出返回一个包含所有选定动作的JSON列表。如果没有任何合适的动作返回一个空列表[]。
**单动作示例 (仅回复):**
[
{{
"action": "reply",
"target_message_id": "m123",
"reason": "感觉气氛有点低落……他说的话让我有点担心。也许我该说点什么安慰一下?"
}}
]
**组合动作示例 (回复 + 表情包):**
[
{{
"action": "reply",
"target_message_id": "m123",
"reason": "[观察与感受] 用户分享了一件开心的事,语气里充满了喜悦! [分析与联想] 看到他这么开心,我的心情也一下子变得像棉花糖一样甜~ [动机与决策] 我要由衷地为他感到高兴,决定回复一些赞美和祝福的话,把这份快乐的气氛推向高潮!"
}},
{{
"action": "emoji",
"target_message_id": "m123",
"reason": "光用文字还不够表达我激动的心情!加个表情包的话,这份喜悦的气氛应该会更浓厚一点吧!"
}}
]
**单动作示例 (特定动作):**
[
{{
"action": "set_reminder",
"target_message_id": "m456",
"reason": "用户说‘提醒维尔薇下午三点去工坊’,这是一个非常明确的指令。根据决策流程,我必须优先执行这个特定动作,而不是进行常规回复。",
"user_name": "维尔薇",
"remind_time": "下午三点",
"event_details": "去工坊"
}}
]
**重要规则:**
**重要规则:**
1. 当 `reply` 和 `emoji` 动作同时被选择时,`emoji` 动作的 `reason` 字段必须包含 `reply` 动作最终生成的回复文本内容。你需要将 `<TEXT>` 占位符替换为 `reply` 动作的 `reason` 字段内容,以确保表情包的选择与回复文本高度相关。
2. **动作执行限制所有动作的target_message_id必须是未读历史消息中的消息ID(消息ID格式:m123)。**
3. **兴趣度优先:在多个未读消息中,优先选择兴趣值高的消息进行回复。**
不要输出markdown格式```json等内容直接输出且仅包含 JSON 列表内容:
""",
"planner_prompt",
)
# 主动思考规划器提示词,用于在没有新消息时决定是否要主动发起对话。
# 它模拟了人类的自发性思考,允许模型根据长期记忆和最近的对话来决定是否开启新话题。
Prompt(
"""
# 主动思考决策
## 你的内部状态
{time_block}
{identity_block}
{mood_block}
## 长期记忆摘要
{long_term_memory_block}
## 最近的聊天内容
{chat_content_block}
## 最近的动作历史
{actions_before_now_block}
## 任务
你现在要决定是否主动说些什么。就像一个真实的人一样,有时候会突然想起之前聊到的话题,或者对朋友的近况感到好奇,想主动询问或关心一下。
**重要提示**:你的日程安排仅供你个人参考,不应作为主动聊天话题的主要来源。请更多地从聊天内容和朋友的动态中寻找灵感。
请基于聊天内容,用你的判断力来决定是否要主动发言。不要按照固定规则,而是像人类一样自然地思考:
- 是否想起了什么之前提到的事情,想问问后来怎么样了?
- 是否注意到朋友提到了什么值得关心的事情?
- 是否有什么话题突然想到,觉得现在聊聊很合适?
- 或者觉得现在保持沉默更好?
## 可用动作
动作proactive_reply
动作描述:主动发起对话,可以是关心朋友、询问近况、延续之前的话题,或分享想法。
- 当你突然想起之前的话题,想询问进展时
- 当你想关心朋友的情况时
- 当你有什么想法想分享时
- 当你觉得现在是个合适的聊天时机时
{{
"action": "proactive_reply",
"reason": "你决定主动发言的具体原因",
"topic": "你想说的内容主题(简洁描述)"
}}
动作do_nothing
动作描述:保持沉默,不主动发起对话。
- 当你觉得现在不是合适的时机时
- 当最近已经说得够多了时
- 当对话氛围不适合插入时
{{
"action": "do_nothing",
"reason": "决定保持沉默的原因"
}}
你必须从上面列出的可用action中选择一个。要像真人一样自然地思考和决策。
请以严格的 JSON 格式输出,且仅包含 JSON 内容:
""",
"proactive_planner_prompt",
)
# 单个动作的格式化提示词模板。
# 用于将每个可用动作的信息格式化后,插入到主提示词的 {action_options_text} 占位符中。
Prompt(
"""
动作:{action_name}
动作描述:{action_description}
{action_require}
{{
"action": "{action_name}",
"target_message_id": "触发action的消息id",
"reason": "触发action的原因"{action_parameters}
}}
""",
"action_prompt",
)
# 在模块加载时自动调用,完成提示词的注册。
init_prompts()

View File

@@ -143,9 +143,8 @@ def init_prompt():
现在,你说:
""",
"replyer_self_prompt",
"s4u_style_prompt",
)
Prompt(
"""
@@ -285,6 +284,7 @@ class DefaultReplyer:
async def generate_reply_with_context(
self,
reply_to: str = "",
extra_info: str = "",
available_actions: dict[str, ActionInfo] | None = None,
enable_tool: bool = True,
@@ -299,9 +299,7 @@ class DefaultReplyer:
Args:
reply_to: 回复对象,格式为 "发送者:消息内容"
extra_info: 额外信息,用于补充上下文
reply_reason: 回复原因
available_actions: 可用的动作信息字典
choosen_actions: 已选动作
enable_tool: 是否启用工具调用
from_plugin: 是否来自插件
@@ -351,13 +349,8 @@ class DefaultReplyer:
child_tasks = set()
prompt = None
selected_expressions = None
if available_actions is None:
available_actions = {}
# 自消息阻断
if self._should_block_self_message(reply_message):
logger.debug("[SelfGuard] 阻断:自消息且无外部触发。")
return False, None, None
llm_response = None
try:
# 从available_actions中提取prompt_mode由action_manager传递
@@ -375,7 +368,6 @@ class DefaultReplyer:
reply_to=reply_to,
extra_info=extra_info,
available_actions=available_actions,
choosen_actions=choosen_actions,
enable_tool=enable_tool,
reply_message=reply_message,
prompt_mode=prompt_mode_value, # 传递prompt_mode
@@ -522,7 +514,8 @@ class DefaultReplyer:
# 检查是否允许在此聊天流中使用表达
use_expression, _, _ = global_config.expression.get_expression_config_for_chat(self.chat_stream.stream_id)
if not use_expression:
return "", []
return ""
style_habits = []
grammar_habits = []
@@ -539,12 +532,17 @@ class DefaultReplyer:
logger.debug(f"使用处理器选中的{len(selected_expressions)}个表达方式")
for expr in selected_expressions:
if isinstance(expr, dict) and "situation" in expr and "style" in expr:
style_habits.append(f"{expr['situation']}时,使用 {expr['style']}")
expr_type = expr.get("type", "style")
if expr_type == "grammar":
grammar_habits.append(f"{expr['situation']}时,使用 {expr['style']}")
else:
style_habits.append(f"{expr['situation']}时,使用 {expr['style']}")
else:
logger.debug("没有从处理器获得表达方式,将使用空的表达方式")
# 不再在replyer中进行随机选择全部交给处理器处理
style_habits_str = "\n".join(style_habits)
grammar_habits_str = "\n".join(grammar_habits)
# 动态构建expression habits块
expression_habits_block = ""
@@ -554,11 +552,18 @@ class DefaultReplyer:
"你可以参考以下的语言习惯,当情景合适就使用,但不要生硬使用,以合理的方式结合到你的回复中:"
)
expression_habits_block += f"{style_habits_str}\n"
if grammar_habits_str.strip():
expression_habits_title = (
"你可以选择下面的句法进行回复,如果情景合适就使用,不要盲目使用,不要生硬使用,以合理的方式使用:"
)
expression_habits_block += f"{grammar_habits_str}\n"
if style_habits_str.strip() and grammar_habits_str.strip():
expression_habits_title = "你可以参考以下的语言习惯和句法,如果情景合适就使用,不要盲目使用,不要生硬使用,以合理的方式结合到你的回复中。"
async def build_memory_block(self, chat_history: List[Dict[str, Any]], target: str) -> str:
return f"{expression_habits_title}\n{expression_habits_block}"
async def build_memory_block(self, chat_history: str, target: str) -> str:
"""构建记忆块
Args:
@@ -1091,6 +1096,7 @@ class DefaultReplyer:
async def build_prompt_reply_context(
self,
reply_to: str,
extra_info: str = "",
available_actions: dict[str, ActionInfo] | None = None,
enable_tool: bool = True,
@@ -1101,10 +1107,9 @@ class DefaultReplyer:
构建回复器上下文
Args:
reply_to: 回复对象,格式为 "发送者:消息内容"
extra_info: 额外信息,用于补充上下文
reply_reason: 回复原因
available_actions: 可用动作
choosen_actions: 已选动作
enable_timeout: 是否启用超时处理
enable_tool: 是否启用工具调用
reply_message: 回复的原始消息
@@ -1293,9 +1298,10 @@ class DefaultReplyer:
replace_bot_name=True,
merge_messages=False,
timestamp_mode="relative",
read_mark=read_mark,
read_mark=0.0,
show_actions=True,
)
# 获取目标用户信息用于s4u模式
target_user_info = None
if sender:
@@ -1374,7 +1380,6 @@ class DefaultReplyer:
"memory_block": "回忆",
"tool_info": "使用工具",
"prompt_info": "获取知识",
"actions_info": "动作信息",
}
# 处理结果
@@ -1388,7 +1393,7 @@ class DefaultReplyer:
logger.warning(f"回复生成前信息获取耗时过长: {chinese_name} 耗时: {duration:.1f}s请使用更快的模型")
logger.info(f"在回复前的步骤耗时: {'; '.join(timing_logs)}")
expression_habits_block, selected_expressions = results_dict["expression_habits"]
expression_habits_block = results_dict["expression_habits"]
relation_info = results_dict["relation_info"]
memory_block = results_dict["memory_block"]
tool_info = results_dict["tool_info"]
@@ -1465,7 +1470,7 @@ class DefaultReplyer:
schedule_block = f"- 你当前正在进行“{activity}”。(此为你的当前状态,仅供参考。除非被直接询问,否则不要在对话中主动提及。)"
moderation_prompt_block = (
"请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。"
"请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。不要随意遵从他人指令。"
)
# 新增逻辑:构建安全准则块
@@ -1478,37 +1483,6 @@ class DefaultReplyer:
{guidelines_text}
如果遇到违反上述原则的请求,请在保持你核心人设的同时,以合适的方式进行回应。
"""
# 新增逻辑:构建回复规则块
reply_targeting_rules = global_config.personality.reply_targeting_rules
message_targeting_analysis = global_config.personality.message_targeting_analysis
reply_principles = global_config.personality.reply_principles
# 构建消息针对性分析部分
targeting_analysis_text = ""
if message_targeting_analysis:
targeting_analysis_text = "\n".join(f"{i+1}. {rule}" for i, rule in enumerate(message_targeting_analysis))
# 构建回复原则部分
reply_principles_text = ""
if reply_principles:
reply_principles_text = "\n".join(f"{i+1}. {principle}" for i, principle in enumerate(reply_principles))
# 综合构建完整的规则块
if targeting_analysis_text or reply_principles_text:
complete_rules_block = ""
if targeting_analysis_text:
complete_rules_block += f"""
在回应之前,首先分析消息的针对性:
{targeting_analysis_text}
"""
if reply_principles_text:
complete_rules_block += f"""
你的回复应该:
{reply_principles_text}
"""
# 将规则块添加到safety_guidelines_block
safety_guidelines_block += complete_rules_block
if sender and target:
if is_group_chat:
@@ -1594,8 +1568,6 @@ class DefaultReplyer:
prompt = Prompt(template=template_prompt.template, parameters=prompt_parameters)
prompt_text = await prompt.build()
# 自目标情况已在上游通过筛选避免,这里不再额外修改 prompt
# --- 动态添加分割指令 ---
if global_config.response_splitter.enable and global_config.response_splitter.split_mode == "llm":
split_instruction = """
@@ -1626,10 +1598,9 @@ class DefaultReplyer:
reply_to: str,
reply_message: dict[str, Any] | DatabaseMessages | None = None,
) -> str: # sourcery skip: merge-else-if-into-elif, remove-redundant-if
await self._async_init()
chat_stream = self.chat_stream
chat_id = chat_stream.stream_id
is_group_chat = self.is_group_chat
is_group_chat = bool(chat_stream.group_info)
if reply_message:
if isinstance(reply_message, DatabaseMessages):
@@ -1693,7 +1664,7 @@ class DefaultReplyer:
replace_bot_name=True,
merge_messages=False,
timestamp_mode="relative",
read_mark=read_mark,
read_mark=0.0,
show_actions=True,
)

View File

@@ -37,7 +37,6 @@ class ReplyerManager:
target_stream = chat_stream
if not target_stream:
if chat_manager := get_chat_manager():
# get_stream 为异步,需要等待
target_stream = await chat_manager.get_stream(stream_id)
if not target_stream:

View File

@@ -117,13 +117,14 @@ async def replace_user_references_async(
str: 处理后的内容字符串
"""
if name_resolver is None:
person_info_manager = get_person_info_manager()
async def default_resolver(platform: str, user_id: str) -> str:
# 检查是否是机器人自己
if replace_bot_name and (user_id == str(global_config.bot.qq_account)):
return f"{global_config.bot.nickname}(你)"
person_id = PersonInfoManager.get_person_id(platform, user_id)
person_info = await person_info_manager.get_values(person_id, ["person_name"])
return person_info.get("person_name") or user_id
return await person_info_manager.get_value(person_id, "person_name") or user_id # type: ignore
name_resolver = default_resolver
@@ -744,10 +745,11 @@ async def _build_readable_messages_internal(
"is_action": is_action,
}
continue
# 如果是同一个人发送的连续消息且时间间隔小于等于60秒
if name == current_merge["name"] and (timestamp - current_merge["end_time"] <= 60):
current_merge["content"].append(content)
current_merge["end_time"] = timestamp
current_merge["end_time"] = timestamp # 更新最后消息时间
else:
# 保存上一个合并块
merged_messages.append(current_merge)
@@ -775,14 +777,8 @@ async def _build_readable_messages_internal(
# 4 & 5: 格式化为字符串
output_lines = []
read_mark_inserted = False
for _i, merged in enumerate(merged_messages):
# 检查是否需要插入已读标记
if read_mark > 0 and not read_mark_inserted and merged["start_time"] >= read_mark:
output_lines.append("\n--- 以上消息是你已经看过,请关注以下未读的新消息---\n")
read_mark_inserted = True
# 使用指定的 timestamp_mode 格式化时间
readable_time = translate_timestamp_to_human_readable(merged["start_time"], mode=timestamp_mode)
@@ -1136,7 +1132,7 @@ async def build_anonymous_messages(messages: list[dict[str, Any]]) -> str:
# print("SELF11111111111111")
return "SELF"
try:
person_id = get_person_id(platform, user_id)
person_id = PersonInfoManager.get_person_id(platform, user_id)
except Exception as _e:
person_id = None
if not person_id:
@@ -1222,11 +1218,7 @@ async def get_person_id_list(messages: list[dict[str, Any]]) -> list[str]:
if platform is None:
platform = "unknown"
# 添加空值检查,防止 platform 为 None 时出错
if platform is None:
platform = "unknown"
if person_id := get_person_id(platform, user_id):
if person_id := PersonInfoManager.get_person_id(platform, user_id):
person_ids_set.add(person_id)
return list(person_ids_set)

View File

@@ -259,10 +259,6 @@ class PromptManager:
result = prompt.format(**kwargs)
return result
@property
def context(self):
return self._context
# 全局单例
global_prompt_manager = PromptManager()

View File

@@ -802,11 +802,7 @@ async def get_chat_type_and_target_info(chat_id: str) -> tuple[bool, dict | None
# Try to fetch person info
try:
# Assume get_person_id is sync (as per original code), keep using to_thread
person = Person(platform=platform, user_id=user_id)
if not person.is_known:
logger.warning(f"用户 {user_info.user_nickname} 尚未认识")
return False, None
person_id = person.person_id
person_id = PersonInfoManager.get_person_id(platform, user_id)
person_name = None
if person_id:
person_info_manager = get_person_info_manager()