from typing import Dict, List import json import os from dotenv import load_dotenv import sys import toml import random from tqdm import tqdm # 添加项目根目录到 Python 路径 root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) sys.path.append(root_path) # 加载配置文件 config_path = os.path.join(root_path, "config", "bot_config.toml") 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 LLM_request_off #noqa E402 # 加载环境变量 env_path = os.path.join(root_path, ".env") if os.path.exists(env_path): print(f"从 {env_path} 加载环境变量") load_dotenv(env_path) else: print(f"未找到环境变量文件: {env_path}") print("将使用默认配置") def adapt_scene(scene: str) -> str: personality_core = config['personality']['personality_core'] personality_sides = config['personality']['personality_sides'] personality_side = random.choice(personality_sides) identity_details = config['identity']['identity_detail'] identity_detail = random.choice(identity_details) """ 根据config中的属性,改编场景使其更适合当前角色 Args: scene: 原始场景描述 Returns: str: 改编后的场景描述 """ try: prompt = f""" 这是一个参与人格测评的角色形象: - 昵称: {config['bot']['nickname']} - 性别: {config['identity']['gender']} - 年龄: {config['identity']['age']}岁 - 外貌: {config['identity']['appearance']} - 性格核心: {personality_core} - 性格侧面: {personality_side} - 身份细节: {identity_detail} 请根据上述形象,改编以下场景,在测评中,用户将根据该场景给出上述角色形象的反应: {scene} 保持场景的本质不变,但最好贴近生活且具体,并且让它更适合这个角色。 改编后的场景应该自然、连贯,并考虑角色的年龄、身份和性格特点。只返回改编后的场景描述,不要包含其他说明。注意{config['bot']['nickname']}是面对这个场景的人,而不是场景的其他人。场景中不会有其描述, 现在,请你给出改编后的场景描述 """ llm = LLM_request_off(model_name=config['model']['llm_normal']['name']) adapted_scene, _ = llm.generate_response(prompt) # 检查返回的场景是否为空或错误信息 if not adapted_scene or "错误" in adapted_scene or "失败" in adapted_scene: print("场景改编失败,将使用原始场景") return scene return adapted_scene except Exception as e: print(f"场景改编过程出错:{str(e)},将使用原始场景") return scene class PersonalityEvaluator_direct: def __init__(self): self.personality_traits = {"开放性": 0, "严谨性": 0, "外向性": 0, "宜人性": 0, "神经质": 0} self.scenarios = [] self.final_scores = {"开放性": 0, "严谨性": 0, "外向性": 0, "宜人性": 0, "神经质": 0} self.dimension_counts = {trait: 0 for trait in self.final_scores.keys()} # 为每个人格特质获取对应的场景 for trait in PERSONALITY_SCENES: scenes = get_scene_by_factor(trait) if not scenes: continue # 从每个维度选择3个场景 import random scene_keys = list(scenes.keys()) selected_scenes = random.sample(scene_keys, min(3, len(scene_keys))) for scene_key in selected_scenes: scene = scenes[scene_key] # 为每个场景添加评估维度 # 主维度是当前特质,次维度随机选择一个其他特质 other_traits = [t for t in PERSONALITY_SCENES if t != trait] secondary_trait = random.choice(other_traits) self.scenarios.append( {"场景": scene["scenario"], "评估维度": [trait, secondary_trait], "场景编号": scene_key} ) self.llm = LLM_request_off() def evaluate_response(self, scenario: str, response: str, dimensions: List[str]) -> Dict[str, float]: """ 使用 DeepSeek AI 评估用户对特定场景的反应 """ # 构建维度描述 dimension_descriptions = [] for dim in dimensions: desc = FACTOR_DESCRIPTIONS.get(dim, "") if desc: dimension_descriptions.append(f"- {dim}:{desc}") dimensions_text = "\n".join(dimension_descriptions) prompt = f"""请根据以下场景和用户描述,评估用户在大五人格模型中的相关维度得分(1-6分)。 场景描述: {scenario} 用户回应: {response} 需要评估的维度说明: {dimensions_text} 请按照以下格式输出评估结果(仅输出JSON格式): {{ "{dimensions[0]}": 分数, "{dimensions[1]}": 分数 }} 评分标准: 1 = 非常不符合该维度特征 2 = 比较不符合该维度特征 3 = 有点不符合该维度特征 4 = 有点符合该维度特征 5 = 比较符合该维度特征 6 = 非常符合该维度特征 请根据用户的回应,结合场景和维度说明进行评分。确保分数在1-6之间,并给出合理的评估。""" try: ai_response, _ = self.llm.generate_response(prompt) # 尝试从AI响应中提取JSON部分 start_idx = ai_response.find("{") end_idx = ai_response.rfind("}") + 1 if start_idx != -1 and end_idx != 0: json_str = ai_response[start_idx:end_idx] scores = json.loads(json_str) # 确保所有分数在1-6之间 return {k: max(1, min(6, float(v))) for k, v in scores.items()} else: print("AI响应格式不正确,使用默认评分") return {dim: 3.5 for dim in dimensions} except Exception as e: print(f"评估过程出错:{str(e)}") return {dim: 3.5 for dim in dimensions} def run_evaluation(self): """ 运行整个评估过程 """ print(f"欢迎使用{config['bot']['nickname']}形象创建程序!") print("接下来,将给您呈现一系列有关您bot的场景(共15个)。") print("请想象您的bot在以下场景下会做什么,并描述您的bot的反应。") print("每个场景都会进行不同方面的评估。") print("\n角色基本信息:") print(f"- 昵称:{config['bot']['nickname']}") print(f"- 性格核心:{config['personality']['personality_core']}") print(f"- 性格侧面:{config['personality']['personality_sides']}") print(f"- 身份细节:{config['identity']['identity_detail']}") print("\n准备好了吗?按回车键开始...") input() total_scenarios = len(self.scenarios) progress_bar = tqdm(total=total_scenarios, desc="场景进度", ncols=100, bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]') for _i, scenario_data in enumerate(self.scenarios, 1): # print(f"\n{'-' * 20} 场景 {i}/{total_scenarios} - {scenario_data['场景编号']} {'-' * 20}") # 改编场景,使其更适合当前角色 print(f"{config['bot']['nickname']}祈祷中...") adapted_scene = adapt_scene(scenario_data["场景"]) scenario_data["改编场景"] = adapted_scene print(adapted_scene) print(f"\n请描述{config['bot']['nickname']}在这种情况下会如何反应:") response = input().strip() if not response: print("反应描述不能为空!") continue print("\n正在评估您的描述...") scores = self.evaluate_response(adapted_scene, response, scenario_data["评估维度"]) # 更新最终分数 for dimension, score in scores.items(): self.final_scores[dimension] += score self.dimension_counts[dimension] += 1 print("\n当前评估结果:") print("-" * 30) for dimension, score in scores.items(): print(f"{dimension}: {score}/6") # 更新进度条 progress_bar.update(1) # if i < total_scenarios: # print("\n按回车键继续下一个场景...") # input() progress_bar.close() # 计算平均分 for dimension in self.final_scores: if self.dimension_counts[dimension] > 0: self.final_scores[dimension] = round(self.final_scores[dimension] / self.dimension_counts[dimension], 2) print("\n" + "=" * 50) print(f" {config['bot']['nickname']}的人格特征评估结果 ".center(50)) print("=" * 50) for trait, score in self.final_scores.items(): print(f"{trait}: {score}/6".ljust(20) + f"测试场景数:{self.dimension_counts[trait]}".rjust(30)) print("=" * 50) # 返回评估结果 return self.get_result() def get_result(self): """ 获取评估结果 """ return { "final_scores": self.final_scores, "dimension_counts": self.dimension_counts, "scenarios": self.scenarios, "bot_info": { "nickname": config['bot']['nickname'], "gender": config['identity']['gender'], "age": config['identity']['age'], "height": config['identity']['height'], "weight": config['identity']['weight'], "appearance": config['identity']['appearance'], "personality_core": config['personality']['personality_core'], "personality_sides": config['personality']['personality_sides'], "identity_detail": config['identity']['identity_detail'] } } def main(): evaluator = PersonalityEvaluator_direct() result = evaluator.run_evaluation() # 准备简化的结果数据 simplified_result = { "openness": round(result["final_scores"]["开放性"] / 6, 1), # 转换为0-1范围 "conscientiousness": round(result["final_scores"]["严谨性"] / 6, 1), "extraversion": round(result["final_scores"]["外向性"] / 6, 1), "agreeableness": round(result["final_scores"]["宜人性"] / 6, 1), "neuroticism": round(result["final_scores"]["神经质"] / 6, 1), "bot_nickname": config['bot']['nickname'] } # 确保目录存在 save_dir = os.path.join(root_path, "data", "personality") os.makedirs(save_dir, exist_ok=True) # 创建文件名,替换可能的非法字符 bot_name = config['bot']['nickname'] # 替换Windows文件名中不允许的字符 for char in ['\\', '/', ':', '*', '?', '"', '<', '>', '|']: bot_name = bot_name.replace(char, '_') file_name = f"{bot_name}_personality.per" save_path = os.path.join(save_dir, file_name) # 保存简化的结果 with open(save_path, "w", encoding="utf-8") as f: json.dump(simplified_result, f, ensure_ascii=False, indent=4) print(f"\n结果已保存到 {save_path}") # 同时保存完整结果到results目录 os.makedirs("results", exist_ok=True) with open("results/personality_result.json", "w", encoding="utf-8") as f: json.dump(result, f, ensure_ascii=False, indent=2) if __name__ == "__main__": main()