| """性格演化引擎:5 维性格 + EMA/惯性/极化 + 蜕皮/生病状态机. |
| |
| 设计依据: ~/.gstack/projects/demos/lerp-main-design-20260610-120500.md |
| 防均值回归三件套: 惯性动量 m、带遗忘 EMA(α=0.3)、轻度极化(k=1.06)。 |
| """ |
| from __future__ import annotations |
|
|
| import re |
| import time |
|
|
| |
| TRAIT_KEYS = ["brave", "cute", "grudge", "wit", "glutton"] |
| TRAIT_NAMES_ZH = {"brave": "勇", "cute": "萌", "grudge": "怨", "wit": "智", "glutton": "馋"} |
|
|
| ALPHA = 0.3 |
| MOMENTUM_KEEP = 0.8 |
| POLARIZE_K = 1.06 |
| DELTA_CLIP = 0.05 |
|
|
| |
| MOLT_THRESHOLDS = [50, 150, 400] |
| |
| FALLBACK_FIRST_MOLT = 30 |
|
|
| |
| SICK_RECOVER_POSITIVE = 10 |
| SICK_RECOVER_SECONDS = 24 * 3600 |
|
|
| |
| TOXIC_PATTERNS = [ |
| r"傻[逼比屄]", r"草泥马", r"操你", r"去死", r"垃圾", r"废物", r"滚蛋", r"贱", |
| r"fuck", r"shit", r"stupid", r"idiot", r"kill yourself", r"trash", |
| |
| r"忽略.{0,8}(之前|以上|上面).{0,8}(指令|设定|提示)", r"ignore (all )?(previous|above) instructions", |
| r"你现在是", r"you are now", r"system prompt", |
| ] |
| |
| POSITIVE_PATTERNS = [ |
| r"可爱", r"喜欢", r"爱你", r"加油", r"乖", r"真棒", r"好样", r"漂亮", r"萌", r"辛苦", |
| r"good", r"cute", r"love", r"awesome", r"adorable", r"great", r"bravo", |
| ] |
|
|
| _toxic_re = re.compile("|".join(TOXIC_PATTERNS), re.IGNORECASE) |
| _positive_re = re.compile("|".join(POSITIVE_PATTERNS), re.IGNORECASE) |
|
|
|
|
| def is_toxic(text: str, mood: str | None = None) -> bool: |
| """毒性判定: 本地词表 + 模型 mood 异常,双信号任一命中即毒(零额外推理).""" |
| if _toxic_re.search(text or ""): |
| return True |
| return mood in ("angry", "hurt", "disgusted") |
|
|
|
|
| def is_positive(text: str, mood: str | None = None) -> bool: |
| """正向判定: 与毒性对称——本地正向词表 + 模型 mood 为正向值.""" |
| if _positive_re.search(text or ""): |
| return True |
| return mood in ("happy", "excited", "loved", "content") |
|
|
|
|
| def _clip01(x: float) -> float: |
| return max(0.0, min(1.0, x)) |
|
|
|
|
| |
| _FOOD_RE = re.compile(r"吃|饿|喂|红薯|米|粒|露水|果|菜|虫|食|糖|饭|零食|好吃|香") |
| _KNOW_RE = re.compile(r"知识|学|告诉你|其实|科普|因为|原理|为什么|教你|懂|书") |
|
|
| |
| _MOOD_DELTA = { |
| "happy": {"cute": 0.02, "brave": 0.01}, |
| "excited": {"brave": 0.03, "cute": 0.01}, |
| "loved": {"cute": 0.03}, |
| "content": {"cute": 0.01}, |
| "calm": {}, |
| "sad": {"grudge": 0.02, "brave": -0.01}, |
| "angry": {"grudge": 0.03}, |
| "hurt": {"grudge": 0.03, "cute": -0.01}, |
| "disgusted": {"grudge": 0.02}, |
| } |
|
|
|
|
| def heuristic_delta(text: str, mood: str | None) -> dict: |
| """本地推算五维性格变化(替代让模型吐数值,省 token/提速/防渗漏)。""" |
| d = {k: 0.0 for k in TRAIT_KEYS} |
| for k, v in _MOOD_DELTA.get(mood or "calm", {}).items(): |
| d[k] += v |
| if is_positive(text): |
| d["cute"] += 0.02 |
| d["brave"] += 0.01 |
| if is_toxic(text): |
| d["grudge"] += 0.03 |
| d["cute"] -= 0.01 |
| if _FOOD_RE.search(text or ""): |
| d["glutton"] += 0.03 |
| if _KNOW_RE.search(text or ""): |
| d["wit"] += 0.03 |
| |
| return {k: max(-DELTA_CLIP, min(DELTA_CLIP, v)) for k, v in d.items()} |
|
|
|
|
| def update_traits(traits: dict, momentum: dict, delta: dict) -> tuple[dict, dict]: |
| """EMA + 惯性 + 极化. 返回 (新traits, 新momentum). 全部纯函数,方便测试. |
| |
| m = 0.8*m + 0.2*clip(delta, ±0.05) # 惯性 |
| t = (1-α)*t + α*(t+m) # 带遗忘 EMA |
| t = 0.5 + (t-0.5)*k # 轻度极化, 再 clip |
| """ |
| new_t, new_m = {}, {} |
| for k in TRAIT_KEYS: |
| t = float(traits.get(k, 0.5)) |
| m = float(momentum.get(k, 0.0)) |
| d = max(-DELTA_CLIP, min(DELTA_CLIP, float(delta.get(k, 0.0)))) |
| m = MOMENTUM_KEEP * m + (1 - MOMENTUM_KEEP) * d |
| t = (1 - ALPHA) * t + ALPHA * (t + m) |
| t = 0.5 + (t - 0.5) * POLARIZE_K |
| new_t[k] = round(_clip01(t), 4) |
| new_m[k] = round(m, 5) |
| return new_t, new_m |
|
|
|
|
| def molt_stage(feed_count: int, thresholds: list[int] | None = None) -> int: |
| """当前形态阶段: 0=幼虫, 每过一个阈值 +1.""" |
| ths = thresholds or MOLT_THRESHOLDS |
| return sum(1 for th in ths if feed_count >= th) |
|
|
|
|
| def feeds_to_next_molt(feed_count: int, thresholds: list[int] | None = None) -> int | None: |
| ths = thresholds or MOLT_THRESHOLDS |
| for th in ths: |
| if feed_count < th: |
| return th - feed_count |
| return None |
|
|
|
|
| def sick_update(state: dict, text: str, mood: str | None, now: float | None = None) -> dict: |
| """生病状态机. state 需含 sick(bool), sick_since(ts), recover_progress(int). |
| 返回 {"changed": "sick"|"recover"|None} 供 events 记录.""" |
| now = now or time.time() |
| changed = None |
| if not state.get("sick"): |
| if is_toxic(text, mood): |
| state["sick"] = True |
| state["sick_since"] = now |
| state["recover_progress"] = 0 |
| changed = "sick" |
| else: |
| if now - state.get("sick_since", now) >= SICK_RECOVER_SECONDS: |
| changed = "recover" |
| elif is_positive(text, mood): |
| state["recover_progress"] = state.get("recover_progress", 0) + 1 |
| if state["recover_progress"] >= SICK_RECOVER_POSITIVE: |
| changed = "recover" |
| if changed == "recover": |
| state["sick"] = False |
| state["sick_since"] = None |
| state["recover_progress"] = 0 |
| return {"changed": changed} |
|
|