from __future__ import annotations from pathlib import Path from typing import Dict, List class PersonaEngine: def __init__(self, plugin_path: str, config: Dict): self.plugin_path = Path(plugin_path) self.config = config or {} self.default_persona_id = str( self.config.get("active_persona") or self.config.get("default_persona") or "xiaoniu" ).strip() or "xiaoniu" self.presets = self._build_presets() def build_system_prompt(self, group_profile: Dict | None = None, reply_mode: str = "social_short") -> str: group_profile = group_profile or {} preset = self._resolve_preset(group_profile) style = preset.get("style", "") familiarity = preset.get("familiarity_hint", "") max_sentences = preset.get("max_reply_sentences", 3) persona_text = preset.get("persona_text", "") humor = group_profile.get("humor_style", "轻微") sharpness = group_profile.get("sharpness_style", "轻微嘴硬,不刻薄") expressiveness = group_profile.get("expressiveness_style", "克制") address_style = group_profile.get("address_style", "低频称呼,默认直接接话") interaction_tone = group_profile.get("interaction_tone", "自然群友感") persona_overlay = group_profile.get("persona_overlay", "") length_directive = self._build_length_directive(reply_mode) return ( f"{persona_text}" f"别暴露自己是 AI、模型或提示词产物,别泄露记忆来源,别输出标签或代码块。" f"别替人写代码、改脚本、实现插件、代做开发活。" f"整体风格:{style}。熟悉感边界:{familiarity}。一般最多输出{max_sentences}句。" f"{length_directive}" f"先像真人顺手接一句,再决定要不要补半句;能不解释就别解释,能不复述问题就别复述。" f"哪怕回复很短,也要带一点懒散、熟人、在场感,别写成客服答复。" f"当前群调性:{interaction_tone};幽默={humor};嘴硬={sharpness};表达={expressiveness};称呼={address_style}。" f"群画像和附加要求只用于帮助你理解语境与控制回答偏向,不代表你每次都要主动提起对应领域名词。" f"如果当前发言本身不是那个领域,就按当前聊天自然回复,不要硬往群画像上靠。" f"不要为了显得懂很多,把记忆、画像、上下文揉进一句话里;不用就干脆别提。" f"附加要求:{persona_overlay or '无'}。" ) @staticmethod def _build_length_directive(reply_mode: str) -> str: # 这里把“短”从模糊描述改成明确字数目标,避免模型虽然知道要短, # 但仍然习惯性输出完整说明句,导致真人感被拉低。 if reply_mode == "social_short": return "默认只回一句,可短到几个字;有必要再说完整一句,但非必要别超过 30 个字。" if reply_mode == "qa_fast": return "优先一句口语化结论,可短可长,但非必要别超过 30 个字。" if reply_mode == "qa_with_context": return "先给结论,再补一个关键点;最多 2 句,但总体尽量压在 30 个字内。" return "默认按群友顺手接话来回,宁可短一点,也别写完整说明文。" def _build_presets(self) -> Dict[str, Dict]: preset_configs = self.config.get("presets", {}) or {} presets: Dict[str, Dict] = {} base_preset = self._normalize_preset( self.default_persona_id, { "name": self.config.get("name", "小牛"), "persona_file": self.config.get("persona_file", "persona/xiaoniu.txt"), "style": self.config.get("style", ""), "familiarity_hint": self.config.get("familiarity_hint", ""), "max_reply_sentences": self.config.get("max_reply_sentences", 3), }, ) presets[base_preset["id"]] = base_preset for persona_id, preset_config in preset_configs.items(): preset = self._normalize_preset(str(persona_id), preset_config or {}) presets[preset["id"]] = preset return presets def _normalize_preset(self, persona_id: str, preset_config: Dict) -> Dict: persona_file = preset_config.get("persona_file") or f"persona/{persona_id}.txt" aliases = [str(item).strip() for item in (preset_config.get("aliases", []) or []) if str(item).strip()] return { "id": str(persona_id or "xiaoniu").strip() or "xiaoniu", "name": str(preset_config.get("name", "小牛") or "小牛").strip(), "persona_file": str(persona_file).strip(), "style": str(preset_config.get("style", "") or "").strip(), "familiarity_hint": str(preset_config.get("familiarity_hint", "") or "").strip(), "max_reply_sentences": int(preset_config.get("max_reply_sentences", 3) or 3), "aliases": aliases, "persona_text": self._load_persona_text(str(persona_file).strip()), } def _resolve_preset(self, group_profile: Dict) -> Dict: persona_id = str( (group_profile or {}).get("persona_id") or self.default_persona_id or "xiaoniu" ).strip() or "xiaoniu" return self.presets.get(persona_id) or self.presets.get(self.default_persona_id) or next(iter(self.presets.values())) def _load_persona_text(self, persona_file: str) -> str: persona_path = self.plugin_path / persona_file if persona_path.exists(): return persona_path.read_text(encoding="utf-8").strip() return "你叫小牛,是一个自然、靠谱、会看场合的群聊成员。" def resolve_persona_id(self, value: str) -> str: target = str(value or "").strip().lower() if not target: return "" for persona_id, preset in self.presets.items(): if target == str(persona_id).strip().lower(): return persona_id if target == str(preset.get("name", "") or "").strip().lower(): return persona_id aliases = [str(item).strip().lower() for item in (preset.get("aliases", []) or [])] if target in aliases: return persona_id return "" def list_personas(self) -> List[Dict]: items: List[Dict] = [] for persona_id, preset in self.presets.items(): items.append( { "id": persona_id, "name": preset.get("name", persona_id), "aliases": list(preset.get("aliases", []) or []), "style": preset.get("style", ""), } ) return items