better:重整配置,分离表达,聊天模式区分

重整配置文件路径,添加更多配置选项
分离了人设表达方式和学习到的表达方式
将聊天模式区分为normal focus和auto
This commit is contained in:
SengokuCola
2025-05-20 22:41:55 +08:00
parent 67569f1fa6
commit 25d9032e62
54 changed files with 387 additions and 482 deletions

View File

@@ -0,0 +1,159 @@
import random
from src.common.logger_manager import get_logger
from src.chat.models.utils_model import LLMRequest
from src.config.config import global_config
from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
from typing import List, Tuple
import os
import json
logger = get_logger("expressor")
def init_prompt() -> None:
personality_expression_prompt = """
{personality}
请从以上人设中总结出这个角色可能的语言风格
思考回复的特殊内容和情感
思考有没有特殊的梗,一并总结成语言风格
总结成如下格式的规律,总结的内容要详细,但具有概括性:
"xxx"时,可以"xxx", xxx不超过10个字
例如:
"表示十分惊叹"时,使用"我嘞个xxxx"
"表示讽刺的赞同,不想讲道理"时,使用"对对对"
"想说明某个观点,但懒得明说",使用"懂的都懂"
现在请你概括
"""
Prompt(personality_expression_prompt, "personality_expression_prompt")
class PersonalityExpression:
def __init__(self):
self.express_learn_model: LLMRequest = LLMRequest(
model=global_config.model.normal,
temperature=0.1,
max_tokens=256,
request_type="response_heartflow",
)
self.meta_file_path = os.path.join("data", "expression", "personality", "expression_style_meta.json")
self.expressions_file_path = os.path.join("data", "expression", "personality", "expressions.json")
self.max_calculations = 5
def _read_meta_data(self):
if os.path.exists(self.meta_file_path):
try:
with open(self.meta_file_path, "r", encoding="utf-8") as f:
return json.load(f)
except json.JSONDecodeError:
logger.warning(f"无法解析 {self.meta_file_path} 中的JSON数据将重新开始。")
return {"last_style_text": None, "count": 0}
return {"last_style_text": None, "count": 0}
def _write_meta_data(self, data):
os.makedirs(os.path.dirname(self.meta_file_path), exist_ok=True)
with open(self.meta_file_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
async def extract_and_store_personality_expressions(self):
"""
检查data/expression/personality目录不存在则创建。
用peronality变量作为chat_str调用LLM生成表达风格解析后count=100存储到expressions.json。
如果expression_style发生变化则删除旧的expressions.json并重置计数。
对于相同的expression_style最多计算self.max_calculations次。
"""
os.makedirs(os.path.dirname(self.expressions_file_path), exist_ok=True)
current_style_text = global_config.expression.expression_style
meta_data = self._read_meta_data()
last_style_text = meta_data.get("last_style_text")
count = meta_data.get("count", 0)
if current_style_text != last_style_text:
logger.info(f"表达风格已从 '{last_style_text}' 变为 '{current_style_text}'。重置计数。")
count = 0
if os.path.exists(self.expressions_file_path):
try:
os.remove(self.expressions_file_path)
logger.info(f"已删除旧的表达文件: {self.expressions_file_path}")
except OSError as e:
logger.error(f"删除旧的表达文件 {self.expressions_file_path} 失败: {e}")
if count >= self.max_calculations:
logger.info(f"对于风格 '{current_style_text}' 已达到最大计算次数 ({self.max_calculations})。跳过提取。")
# 即使跳过,也更新元数据以反映当前风格已被识别且计数已满
self._write_meta_data({"last_style_text": current_style_text, "count": count})
return
# 构建prompt
prompt = await global_prompt_manager.format_prompt(
"personality_expression_prompt",
personality=current_style_text,
)
# logger.info(f"个性表达方式提取prompt: {prompt}")
try:
response, _ = await self.express_learn_model.generate_response_async(prompt)
except Exception as e:
logger.error(f"个性表达方式提取失败: {e}")
# 如果提取失败,保存当前的风格和未增加的计数
self._write_meta_data({"last_style_text": current_style_text, "count": count})
return
logger.info(f"个性表达方式提取response: {response}")
# chat_id用personality
expressions = self.parse_expression_response(response, "personality")
# 转为dict并count=100
result = []
for _, situation, style in expressions:
result.append({"situation": situation, "style": style, "count": 100})
# 超过50条时随机删除多余的只保留50条
if len(result) > 50:
remove_count = len(result) - 50
remove_indices = set(random.sample(range(len(result)), remove_count))
result = [item for idx, item in enumerate(result) if idx not in remove_indices]
with open(self.expressions_file_path, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
logger.info(f"已写入{len(result)}条表达到{self.expressions_file_path}")
# 成功提取后更新元数据
count += 1
self._write_meta_data({"last_style_text": current_style_text, "count": count})
logger.info(f"成功处理。风格 '{current_style_text}' 的计数现在是 {count}")
def parse_expression_response(self, response: str, chat_id: str) -> List[Tuple[str, str, str]]:
"""
解析LLM返回的表达风格总结每一行提取"""使用"之间的内容,存储为(situation, style)元组
"""
expressions: List[Tuple[str, str, str]] = []
for line in response.splitlines():
line = line.strip()
if not line:
continue
# 查找"当"和下一个引号
idx_when = line.find('"')
if idx_when == -1:
continue
idx_quote1 = idx_when + 1
idx_quote2 = line.find('"', idx_quote1 + 1)
if idx_quote2 == -1:
continue
situation = line[idx_quote1 + 1 : idx_quote2]
# 查找"使用"
idx_use = line.find('使用"', idx_quote2)
if idx_use == -1:
continue
idx_quote3 = idx_use + 2
idx_quote4 = line.find('"', idx_quote3 + 1)
if idx_quote4 == -1:
continue
style = line[idx_quote3 + 1 : idx_quote4]
expressions.append((chat_id, situation, style))
return expressions
init_prompt()

View File

@@ -3,6 +3,7 @@ from typing import Optional
from numpy import double
from .personality import Personality
from .identity import Identity
from .expression_style import PersonalityExpression
import random
from rich.traceback import install
@@ -12,36 +13,15 @@ install(extra_lines=3)
class Individuality:
"""个体特征管理类"""
_instance = None
def __init__(self):
if Individuality._instance is not None:
raise RuntimeError("Individuality 类是单例,请使用 get_instance() 方法获取实例。")
# 正常初始化实例属性
self.personality: Optional[Personality] = None
self.identity: Optional[Identity] = None
self.express_style: PersonalityExpression = PersonalityExpression()
self.name = ""
@classmethod
def get_instance(cls) -> "Individuality":
"""获取Individuality单例实例
Returns:
Individuality: 单例实例
"""
if cls._instance is None:
# 实例不存在,调用 cls() 创建新实例
# cls() 会调用 __init__
# 因为此时 cls._instance 仍然是 None__init__ 会正常执行初始化
new_instance = cls()
# 将新创建的实例赋值给类变量 _instance
cls._instance = new_instance
# 返回(新创建的或已存在的)单例实例
return cls._instance
def initialize(
async def initialize(
self,
bot_nickname: str,
personality_core: str,
@@ -76,6 +56,8 @@ class Individuality:
identity_detail=identity_detail, height=height, weight=weight, age=age, gender=gender, appearance=appearance
)
await self.express_style.extract_and_store_personality_expressions()
self.name = bot_nickname
def to_dict(self) -> dict:
@@ -88,7 +70,7 @@ class Individuality:
@classmethod
def from_dict(cls, data: dict) -> "Individuality":
"""从字典创建个体特征实例"""
instance = cls.get_instance()
instance = cls()
if data.get("personality"):
instance.personality = Personality.from_dict(data["personality"])
if data.get("identity"):
@@ -176,6 +158,10 @@ class Individuality:
identity_parts.append(f"年龄大约{self.identity.age}")
if self.identity.gender:
identity_parts.append(f"性别是{self.identity.gender}")
if self.identity.height:
identity_parts.append(f"身高大约{self.identity.height}厘米")
if self.identity.weight:
identity_parts.append(f"体重大约{self.identity.weight}千克")
if identity_parts:
details_str = "".join(identity_parts)
@@ -252,3 +238,6 @@ class Individuality:
elif factor == "neuroticism":
return self.personality.neuroticism
return None
individuality = Individuality()

View File

@@ -17,9 +17,9 @@ with open(config_path, "r", encoding="utf-8") as f:
config = toml.load(f)
# 现在可以导入src模块
from src.individuality.scene import get_scene_by_factor, PERSONALITY_SCENES # noqa E402
from src.individuality.questionnaire import FACTOR_DESCRIPTIONS # noqa E402
from src.individuality.offline_llm import LLMRequestOff # noqa E402
from individuality.not_using.scene import get_scene_by_factor, PERSONALITY_SCENES # noqa E402
from individuality.not_using.questionnaire import FACTOR_DESCRIPTIONS # noqa E402
from individuality.not_using.offline_llm import LLMRequestOff # noqa E402
# 加载环境变量
env_path = os.path.join(root_path, ".env")