Files
abot/plugins/ai_auto_response/profile/persona_engine.py

132 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 "默认只回一句长度自然浮动0 到 30 个字都可以;能短就短,能说完就行。"
if reply_mode == "qa_fast":
return "优先一句口语化结论长度自然浮动0 到 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