From 80753d95a5bc0c12792ef0c438ad1a75f99d88e1 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sun, 6 Apr 2025 00:58:46 +0800 Subject: [PATCH] =?UTF-8?q?move=EF=BC=9A=E4=BF=AE=E6=94=B9=E4=BA=BA?= =?UTF-8?q?=E6=A0=BC=E6=96=87=E4=BB=B6=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/personality/offline_llm.py | 123 ++++++++++++ src/personality/personality.py | 32 +++ src/personality/personality_gen.py | 186 ++++++++++++++++++ src/plugins/config/config.py | 59 ++++-- .../big5_test.py | 0 .../can_i_recog_u.py | 0 .../combined_test.py | 0 .../offline_llm.py | 0 .../questionnaire.py | 0 .../renqingziji.py | 0 .../renqingziji_with_mymy.py | 0 .../{personality => personality_s}/scene.py | 0 .../{personality => personality_s}/who_r_u.py | 0 .../{personality => personality_s}/看我.txt | 0 template/bot_config_template.toml | 29 ++- 15 files changed, 406 insertions(+), 23 deletions(-) create mode 100644 src/personality/offline_llm.py create mode 100644 src/personality/personality.py create mode 100644 src/personality/personality_gen.py rename src/plugins/{personality => personality_s}/big5_test.py (100%) rename src/plugins/{personality => personality_s}/can_i_recog_u.py (100%) rename src/plugins/{personality => personality_s}/combined_test.py (100%) rename src/plugins/{personality => personality_s}/offline_llm.py (100%) rename src/plugins/{personality => personality_s}/questionnaire.py (100%) rename src/plugins/{personality => personality_s}/renqingziji.py (100%) rename src/plugins/{personality => personality_s}/renqingziji_with_mymy.py (100%) rename src/plugins/{personality => personality_s}/scene.py (100%) rename src/plugins/{personality => personality_s}/who_r_u.py (100%) rename src/plugins/{personality => personality_s}/看我.txt (100%) diff --git a/src/personality/offline_llm.py b/src/personality/offline_llm.py new file mode 100644 index 000000000..8d6820651 --- /dev/null +++ b/src/personality/offline_llm.py @@ -0,0 +1,123 @@ +import asyncio +import os +import time +from typing import Tuple, Union + +import aiohttp +import requests +from src.common.logger import get_module_logger + +logger = get_module_logger("offline_llm") + + +class LLM_request_off: + def __init__(self, model_name="Pro/deepseek-ai/DeepSeek-V3", **kwargs): + self.model_name = model_name + self.params = kwargs + self.api_key = os.getenv("SILICONFLOW_KEY") + self.base_url = os.getenv("SILICONFLOW_BASE_URL") + + if not self.api_key or not self.base_url: + raise ValueError("环境变量未正确加载:SILICONFLOW_KEY 或 SILICONFLOW_BASE_URL 未设置") + + logger.info(f"API URL: {self.base_url}") # 使用 logger 记录 base_url + + def generate_response(self, prompt: str) -> Union[str, Tuple[str, str]]: + """根据输入的提示生成模型的响应""" + headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"} + + # 构建请求体 + data = { + "model": self.model_name, + "messages": [{"role": "user", "content": prompt}], + "temperature": 0.5, + **self.params, + } + + # 发送请求到完整的 chat/completions 端点 + api_url = f"{self.base_url.rstrip('/')}/chat/completions" + logger.info(f"Request URL: {api_url}") # 记录请求的 URL + + max_retries = 3 + base_wait_time = 15 # 基础等待时间(秒) + + for retry in range(max_retries): + try: + response = requests.post(api_url, headers=headers, json=data) + + if response.status_code == 429: + wait_time = base_wait_time * (2**retry) # 指数退避 + logger.warning(f"遇到请求限制(429),等待{wait_time}秒后重试...") + time.sleep(wait_time) + continue + + response.raise_for_status() # 检查其他响应状态 + + result = response.json() + if "choices" in result and len(result["choices"]) > 0: + content = result["choices"][0]["message"]["content"] + reasoning_content = result["choices"][0]["message"].get("reasoning_content", "") + return content, reasoning_content + return "没有返回结果", "" + + except Exception as e: + if retry < max_retries - 1: # 如果还有重试机会 + wait_time = base_wait_time * (2**retry) + logger.error(f"[回复]请求失败,等待{wait_time}秒后重试... 错误: {str(e)}") + time.sleep(wait_time) + else: + logger.error(f"请求失败: {str(e)}") + return f"请求失败: {str(e)}", "" + + logger.error("达到最大重试次数,请求仍然失败") + return "达到最大重试次数,请求仍然失败", "" + + async def generate_response_async(self, prompt: str) -> Union[str, Tuple[str, str]]: + """异步方式根据输入的提示生成模型的响应""" + headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"} + + # 构建请求体 + data = { + "model": self.model_name, + "messages": [{"role": "user", "content": prompt}], + "temperature": 0.5, + **self.params, + } + + # 发送请求到完整的 chat/completions 端点 + api_url = f"{self.base_url.rstrip('/')}/chat/completions" + logger.info(f"Request URL: {api_url}") # 记录请求的 URL + + max_retries = 3 + base_wait_time = 15 + + async with aiohttp.ClientSession() as session: + for retry in range(max_retries): + try: + async with session.post(api_url, headers=headers, json=data) as response: + if response.status == 429: + wait_time = base_wait_time * (2**retry) # 指数退避 + logger.warning(f"遇到请求限制(429),等待{wait_time}秒后重试...") + await asyncio.sleep(wait_time) + continue + + response.raise_for_status() # 检查其他响应状态 + + result = await response.json() + if "choices" in result and len(result["choices"]) > 0: + content = result["choices"][0]["message"]["content"] + reasoning_content = result["choices"][0]["message"].get("reasoning_content", "") + return content, reasoning_content + return "没有返回结果", "" + + except Exception as e: + if retry < max_retries - 1: # 如果还有重试机会 + wait_time = base_wait_time * (2**retry) + logger.error(f"[回复]请求失败,等待{wait_time}秒后重试... 错误: {str(e)}") + await asyncio.sleep(wait_time) + else: + logger.error(f"请求失败: {str(e)}") + return f"请求失败: {str(e)}", "" + + logger.error("达到最大重试次数,请求仍然失败") + return "达到最大重试次数,请求仍然失败", "" diff --git a/src/personality/personality.py b/src/personality/personality.py new file mode 100644 index 000000000..3977743a5 --- /dev/null +++ b/src/personality/personality.py @@ -0,0 +1,32 @@ +from dataclasses import dataclass +from typing import Dict, List + +@dataclass +class Personality: + """人格特质类""" + openness: float # 开放性 + conscientiousness: float # 尽责性 + extraversion: float # 外向性 + agreeableness: float # 宜人性 + neuroticism: float # 神经质 + bot_nickname: str # 机器人昵称 + personality_core: str # 人格核心特点 + personality_detail: List[str] # 人格细节描述 + + def to_dict(self) -> Dict: + """将人格特质转换为字典格式""" + return { + "openness": self.openness, + "conscientiousness": self.conscientiousness, + "extraversion": self.extraversion, + "agreeableness": self.agreeableness, + "neuroticism": self.neuroticism, + "bot_nickname": self.bot_nickname, + "personality_core": self.personality_core, + "personality_detail": self.personality_detail + } + + @classmethod + def from_dict(cls, data: Dict) -> 'Personality': + """从字典创建人格特质实例""" + return cls(**data) \ No newline at end of file diff --git a/src/personality/personality_gen.py b/src/personality/personality_gen.py new file mode 100644 index 000000000..8eaf99db0 --- /dev/null +++ b/src/personality/personality_gen.py @@ -0,0 +1,186 @@ +import os +import json +import sys +from typing import Optional, List + +sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) +from src.personality.offline_llm import LLM_request_off +from src.common.logger import get_module_logger +from src.personality.personality import Personality + +logger = get_module_logger("personality_gen") + +class PersonalityGenerator: + """人格生成器类""" + def __init__(self, bot_nickname: str): + self.bot_nickname = bot_nickname + self.llm = LLM_request_off() + self.personality: Optional[Personality] = None + self.save_path = os.path.join("data", "personality") + + # 确保保存目录存在 + os.makedirs(self.save_path, exist_ok=True) + + def personality_exists(self) -> bool: + """检查是否已存在该机器人的人格文件""" + file_path = os.path.join(self.save_path, f"{self.bot_nickname}_personality.per") + return os.path.exists(file_path) + + async def generate_personality( + self, + personality_core: str, + personality_detail: List[str], + height: int, + weight: int, + age: int, + gender: str, + appearance: str, + interests: List[str], + others: List[str] + ) -> Optional[Personality]: + """根据配置生成人格特质""" + # 检查是否已存在 + if self.personality_exists(): + logger.info(f"机器人 {self.bot_nickname} 的人格文件已存在,跳过生成") + return await self.load_personality() + + # 构建提示文本 + prompt = f"""你是一个心理学家,专职心理测量和大五人格研究。请根据以下信息分析并给出这个人的大五人格特质评分。 +每个特质的分数范围是0到1之间的小数,请确保返回标准的JSON格式。 + +机器人信息: +- 昵称:{self.bot_nickname} +- 性格核心的特质:{personality_core} +- 性格细节:{', '.join(personality_detail)} +- 身高:{height}cm +- 体重:{weight}kg +- 年龄:{age}岁 +- 性别:{gender} +- 外貌:{appearance} +- 兴趣爱好:{', '.join(interests)} +- 其他信息:{', '.join(others)} +请只返回如下JSON格式数据(不要包含任何其他文字): +{{ + "openness": 0.x, + "conscientiousness": 0.x, + "extraversion": 0.x, + "agreeableness": 0.x, + "neuroticism": 0.x +}}""" + + response, _ = await self.llm.generate_response_async(prompt) + try: + # 尝试清理响应文本,只保留JSON部分 + json_str = response.strip() + if "```json" in json_str: + json_str = json_str.split("```json")[1].split("```")[0].strip() + elif "```" in json_str: + json_str = json_str.split("```")[1].strip() + + traits = json.loads(json_str) + + # 验证所有必需的字段是否存在 + required_fields = ["openness", "conscientiousness", "extraversion", "agreeableness", "neuroticism"] + if not all(field in traits for field in required_fields): + raise ValueError("缺少必需的人格特质字段") + + # 验证数值是否在合理范围内 + for field in required_fields: + if not 0 <= traits[field] <= 1: + traits[field] = max(0, min(traits[field], 1)) + + self.personality = Personality( + **traits, + bot_nickname=self.bot_nickname + ) + await self.save_personality() + return self.personality + + except json.JSONDecodeError as e: + logger.error(f"JSON解析失败: {e}\n响应内容: {response}") + raise + except Exception as e: + logger.error(f"生成人格特质失败: {e}") + raise + + async def save_personality(self) -> None: + """保存人格特质到文件""" + if not self.personality: + raise ValueError("没有可保存的人格特质") + + file_path = os.path.join(self.save_path, f"{self.bot_nickname}_personality.per") + try: + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(self.personality.to_dict(), f, ensure_ascii=False, indent=4) + logger.info(f"人格特质已保存到: {file_path}") + except Exception as e: + logger.error(f"保存人格特质失败: {e}") + raise + + async def load_personality(self) -> Optional[Personality]: + """从文件加载人格特质""" + file_path = os.path.join(self.save_path, f"{self.bot_nickname}_personality.per") + try: + if os.path.exists(file_path): + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + self.personality = Personality.from_dict(data) + return self.personality + except Exception as e: + logger.error(f"加载人格特质失败: {e}") + return None + +async def main(): + """主函数,用于测试人格生成""" + # 创建人格生成器实例 + generator = PersonalityGenerator("麦麦") + + # 生成或加载人格 + personality = await generator.generate_personality( + personality_core="对世界抱着善意和好奇,愿意尝试新奇事物", + personality_detail=[ + "你会刷小红书", + "你会刷贴吧", + "学习心理学和脑科学", + "你会刷b站,对ACG文化感兴趣", + "有时候有些搞怪", + ], + height=160, + weight=45, + age=20, + gender="女", + appearance="有着橙色短发", + interests=["摄影", "绘画"], + others=["是一个大二女大学生"] + ) + + if personality: + logger.info("人格特质生成成功:") + logger.info(f"开放性: {personality.openness}") + logger.info(f"尽责性: {personality.conscientiousness}") + logger.info(f"外向性: {personality.extraversion}") + logger.info(f"宜人性: {personality.agreeableness}") + logger.info(f"神经质: {personality.neuroticism}") + else: + logger.error("人格特质生成失败") + +if __name__ == "__main__": + import asyncio + import platform + + if platform.system() == 'Windows': + # Windows平台特殊处理 + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + + try: + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) + finally: + # 确保所有待处理的任务都完成 + pending = asyncio.all_tasks(loop) + for task in pending: + task.cancel() + + # 运行一次以处理取消的任务 + loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True)) + loop.close() diff --git a/src/plugins/config/config.py b/src/plugins/config/config.py index 5b58f2d52..83314c082 100644 --- a/src/plugins/config/config.py +++ b/src/plugins/config/config.py @@ -148,14 +148,36 @@ class BotConfig: ban_user_id = set() # personality - PROMPT_PERSONALITY = [ - "用一句话或几句话描述性格特点和其他特征", - "例如,是一个热爱国家热爱党的新时代好青年", - "例如,曾经是一个学习地质的女大学生,现在学习心理学和脑科学,你会刷贴吧", - ] - PERSONALITY_1: float = 0.6 # 第一种人格概率 - PERSONALITY_2: float = 0.3 # 第二种人格概率 - PERSONALITY_3: float = 0.1 # 第三种人格概率 + personality_core = "用一句话或几句话描述人格的核心特点" # 建议20字以内,谁再写3000字小作文敲谁脑袋 + personality_detail: List[str] = field(default_factory=lambda: [ + "用一句话或几句话描述人格的一些细节", + "用一句话或几句话描述人格的一些细节", + "用一句话或几句话描述人格的一些细节", + "用一句话或几句话描述人格的一些细节", + "用一句话或几句话描述人格的一些细节" + ]) + + traits: List[str] = field(default_factory=lambda: [ + "用一个词描述性格", + "用一个词描述性格", + "用一个词描述性格", + ]) + + # identity + identity_detail: List[str] = field(default_factory=lambda: [ + "身份特点", + "身份特点", + ]) + height: int = 170 # 身高 单位厘米 + weight: int = 50 # 体重 单位千克 + age: int = 20 # 年龄 单位岁 + gender: str = "男" # 性别 + appearance: str = "用几句话描述外貌特征" # 外貌特征 + interests: List[str] = field(default_factory=lambda: [ + "兴趣爱好1", + "兴趣爱好2", + "兴趣爱好3" + ]) # schedule ENABLE_SCHEDULE_GEN: bool = False # 是否启用日程生成 @@ -347,14 +369,20 @@ class BotConfig: def personality(parent: dict): personality_config = parent["personality"] - personality = personality_config.get("prompt_personality") - if len(personality) >= 2: - logger.info(f"载入自定义人格:{personality}") - config.PROMPT_PERSONALITY = personality_config.get("prompt_personality", config.PROMPT_PERSONALITY) + if config.INNER_VERSION in SpecifierSet(">=1.2.4"): + config.personality_core = personality_config.get("personality_core", config.personality_core) + config.personality_detail = personality_config.get("personality_detail", config.personality_detail) - config.PERSONALITY_1 = personality_config.get("personality_1_probability", config.PERSONALITY_1) - config.PERSONALITY_2 = personality_config.get("personality_2_probability", config.PERSONALITY_2) - config.PERSONALITY_3 = personality_config.get("personality_3_probability", config.PERSONALITY_3) + def identity(parent: dict): + identity_config = parent["identity"] + if config.INNER_VERSION in SpecifierSet(">=1.2.4"): + config.identity_detail = identity_config.get("identity_detail", config.identity_detail) + config.height = identity_config.get("height", config.height) + config.weight = identity_config.get("weight", config.weight) + config.age = identity_config.get("age", config.age) + config.gender = identity_config.get("gender", config.gender) + config.appearance = identity_config.get("appearance", config.appearance) + config.interests = identity_config.get("interests", config.interests) def schedule(parent: dict): schedule_config = parent["schedule"] @@ -611,6 +639,7 @@ class BotConfig: "bot": {"func": bot, "support": ">=0.0.0"}, "groups": {"func": groups, "support": ">=0.0.0"}, "personality": {"func": personality, "support": ">=0.0.0"}, + "identity": {"func": identity, "support": ">=1.2.4"}, "schedule": {"func": schedule, "support": ">=0.0.11", "necessary": False}, "message": {"func": message, "support": ">=0.0.0"}, "willing": {"func": willing, "support": ">=0.0.9", "necessary": False}, diff --git a/src/plugins/personality/big5_test.py b/src/plugins/personality_s/big5_test.py similarity index 100% rename from src/plugins/personality/big5_test.py rename to src/plugins/personality_s/big5_test.py diff --git a/src/plugins/personality/can_i_recog_u.py b/src/plugins/personality_s/can_i_recog_u.py similarity index 100% rename from src/plugins/personality/can_i_recog_u.py rename to src/plugins/personality_s/can_i_recog_u.py diff --git a/src/plugins/personality/combined_test.py b/src/plugins/personality_s/combined_test.py similarity index 100% rename from src/plugins/personality/combined_test.py rename to src/plugins/personality_s/combined_test.py diff --git a/src/plugins/personality/offline_llm.py b/src/plugins/personality_s/offline_llm.py similarity index 100% rename from src/plugins/personality/offline_llm.py rename to src/plugins/personality_s/offline_llm.py diff --git a/src/plugins/personality/questionnaire.py b/src/plugins/personality_s/questionnaire.py similarity index 100% rename from src/plugins/personality/questionnaire.py rename to src/plugins/personality_s/questionnaire.py diff --git a/src/plugins/personality/renqingziji.py b/src/plugins/personality_s/renqingziji.py similarity index 100% rename from src/plugins/personality/renqingziji.py rename to src/plugins/personality_s/renqingziji.py diff --git a/src/plugins/personality/renqingziji_with_mymy.py b/src/plugins/personality_s/renqingziji_with_mymy.py similarity index 100% rename from src/plugins/personality/renqingziji_with_mymy.py rename to src/plugins/personality_s/renqingziji_with_mymy.py diff --git a/src/plugins/personality/scene.py b/src/plugins/personality_s/scene.py similarity index 100% rename from src/plugins/personality/scene.py rename to src/plugins/personality_s/scene.py diff --git a/src/plugins/personality/who_r_u.py b/src/plugins/personality_s/who_r_u.py similarity index 100% rename from src/plugins/personality/who_r_u.py rename to src/plugins/personality_s/who_r_u.py diff --git a/src/plugins/personality/看我.txt b/src/plugins/personality_s/看我.txt similarity index 100% rename from src/plugins/personality/看我.txt rename to src/plugins/personality_s/看我.txt diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index d7ec90cd1..c40c03dfd 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -34,14 +34,27 @@ talk_frequency_down = [] #降低回复频率的群号码 ban_user_id = [] #禁止回复和读取消息的QQ号 [personality] -prompt_personality = [ - "用一句话或几句话描述性格特点和其他特征", - "例如,是一个热爱国家热爱党的新时代好青年", - "例如,曾经是一个学习地质的女大学生,现在学习心理学和脑科学,你会刷贴吧" - ] -personality_1_probability = 0.7 # 第一种人格出现概率 -personality_2_probability = 0.2 # 第二种人格出现概率,可以为0 -personality_3_probability = 0.1 # 第三种人格出现概率,请确保三个概率相加等于1 +personality_core = "用一句话或几句话描述人格的核心特点" # 建议20字以内,谁再写3000字小作文敲谁脑袋 +personality_detail = [ + "用一句话或几句话描述人格的一些细节", + "用一句话或几句话描述人格的一些细节", + "用一句话或几句话描述人格的一些细节", + "用一句话或几句话描述人格的一些细节", + "用一句话或几句话描述人格的一些细节", +]# 条数任意 + +[identity] #アイデンティティがない 生まれないらららら +# 兴趣爱好 +identity_detail = [ + "身份特点", + "身份特点", +]# 条数任意 +#外貌特征 +height = 170 # 身高 单位厘米 +weight = 50 # 体重 单位千克 +age = 20 # 年龄 单位岁 +gender = "男" # 性别 +appearance = "用几句话描述外貌特征" # 外貌特征 [schedule] enable_schedule_gen = true # 是否启用日程表(尚未完成)