M6:缘契、人格与世界——契约版本链、铸魂分灵、河图境界与三界分流
- 缘契域 domains/yuanqi:covenants(当前版)+ covenant_events(不可变历史);提议—问心—铭契—撤回全链;边界变更立即生效;多维关系并存,服务层拒绝 affinity/好感度单一键;浪漫维度成人资格标记位 - 道体铸魂与分灵 domains/jingsheng/soul.py:soul_forgings 确认流(锚点变更 pending→confirmed 应用并签发新魂籍版本,确认前人物锚点不变);分灵创建新人物(继承资料与谱系 lineage 链、不继承原身份,魂籍从 v1 起) - 河图域 domains/hetu:persistent_worlds 境界 + world_entities/world_relations 境界作用域重建 + 世谱挂接;境界正史视图(scope=persistent_world 归真集合);canonize 后置推进正史计数 - 三界分流:reality→息壤提取(不变);persistent_world→境界实录+正史推进;simulation→只记试演实录;factual_records 增 scope 列——境界/幻境实录与真界本相隔离 - 正影打通:引文 target_type=memory 时被引记忆 superseded → 返回 successor_memory_id 提示,不静默替换 - 前端:人物详情页缘契面板(提议维度选择/铭契/撤回/事件史)+ 世界页重建(境界目录/实体/正史概览)+ covenants/worlds api 库 - 退役:旧 personality/drive/evolution/world 模块、服务、端点、仓储、schema、前端五页与组件样式;迁移 007 退役九张旧表与 characters.current_personality_version_id - 验收(test_m6_domains 六用例):多维无单一好感度/铭契版本链/事件不可抹除/撤回立即生效/边界即时/铸魂确认流+魂籍版本/分灵谱系/境界正史不进真界(实录 scope 隔离+真界候选=0)/引文正影后继 - 四套件:后端 379、前端 199、BFF 4、神识 5 包全绿(2 个预存环境失败不变);迁移 007 净库+±1 干净
This commit is contained in:
1 parent
684261f4b4
commit
7e23d6fa84
108 files changed
+2600
-9732
No files matched your search
@@ -1 +0,0 @@
|
||||
"""大周天内驱力领域纯逻辑(造消息 / 解析好奇问题与次生目标草稿,禁止 IO)。"""
|
||||
@@ -1,127 +0,0 @@
|
||||
"""解析模型返回为内驱力草稿(纯函数,无 IO)。
|
||||
|
||||
好奇问题可内嵌次生目标([内驱力生成器设计] ../../docs/domain/core-domains.md
|
||||
§6:好奇问题→转化次生目标)。缺省取**安全悲观默认**:``risk_level=L4``、
|
||||
``source=reflection``,避免模型漏字段时生成高风险探索。
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class SubgoalDraft:
|
||||
title: str
|
||||
description: str
|
||||
expected_information_gain: float = 0.5
|
||||
interruption_cost: float = 0.5
|
||||
risk_level: str = "L4"
|
||||
|
||||
|
||||
@dataclass
|
||||
class CuriosityDraft:
|
||||
question: str
|
||||
source: str = "reflection"
|
||||
related_entities: list[str] = field(default_factory=list)
|
||||
expected_value: float = 0.5
|
||||
urgency: float = 0.5
|
||||
risk_level: str = "L4"
|
||||
subgoals: list[SubgoalDraft] = field(default_factory=list)
|
||||
|
||||
|
||||
_VALID_SOURCES = {
|
||||
"world_uncertainty",
|
||||
"memory_gap",
|
||||
"perception_change",
|
||||
"goal_tension",
|
||||
"reflection",
|
||||
"time_rhythm",
|
||||
}
|
||||
_VALID_LEVELS = {"L0", "L1", "L2", "L3", "L4"}
|
||||
|
||||
|
||||
def parse_curiosities(raw: str) -> list[CuriosityDraft]:
|
||||
drafts: list[CuriosityDraft] = []
|
||||
for item in _extract_json_array(raw):
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
question = str(item.get("question", "")).strip()
|
||||
if not question:
|
||||
continue
|
||||
drafts.append(
|
||||
CuriosityDraft(
|
||||
question=question,
|
||||
source=_source(item.get("source")),
|
||||
related_entities=_str_list(item.get("related_entities")),
|
||||
expected_value=_clamp_float(item.get("expected_value"), 0.5),
|
||||
urgency=_clamp_float(item.get("urgency"), 0.5),
|
||||
risk_level=_level(item.get("risk_level")),
|
||||
subgoals=_parse_subgoals(item.get("subgoals")),
|
||||
)
|
||||
)
|
||||
return drafts
|
||||
|
||||
|
||||
def _parse_subgoals(value: Any) -> list[SubgoalDraft]:
|
||||
if not isinstance(value, list):
|
||||
return []
|
||||
result: list[SubgoalDraft] = []
|
||||
for item in value:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
title = str(item.get("title", "")).strip()
|
||||
description = str(item.get("description", "")).strip()
|
||||
if not title or not description:
|
||||
continue
|
||||
result.append(
|
||||
SubgoalDraft(
|
||||
title=title,
|
||||
description=description,
|
||||
expected_information_gain=_clamp_float(item.get("expected_information_gain"), 0.5),
|
||||
interruption_cost=_clamp_float(item.get("interruption_cost"), 0.5),
|
||||
risk_level=_level(item.get("risk_level")),
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _extract_json_array(raw: str) -> list[Any]:
|
||||
text = raw.strip()
|
||||
matched = re.search(r"```(?:json)?\s*(\[.*?\])\s*```", text, re.DOTALL)
|
||||
if matched:
|
||||
text = matched.group(1)
|
||||
else:
|
||||
start, end = text.find("["), text.rfind("]")
|
||||
if start != -1 and end > start:
|
||||
text = text[start : end + 1]
|
||||
try:
|
||||
data = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
return data if isinstance(data, list) else []
|
||||
|
||||
|
||||
def _source(value: Any) -> str:
|
||||
text = str(value or "").strip()
|
||||
return text if text in _VALID_SOURCES else "reflection"
|
||||
|
||||
|
||||
def _level(value: Any) -> str:
|
||||
text = str(value or "").strip().upper()
|
||||
return text if text in _VALID_LEVELS else "L4"
|
||||
|
||||
|
||||
def _str_list(value: Any) -> list[str]:
|
||||
if not isinstance(value, list):
|
||||
return []
|
||||
return [str(v) for v in value if v is not None]
|
||||
|
||||
|
||||
def _clamp_float(value: Any, default: float) -> float:
|
||||
try:
|
||||
num = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
return max(0.0, min(1.0, num))
|
||||
@@ -1,55 +0,0 @@
|
||||
"""大周天内驱力 prompt 构建(纯函数,无 IO)。
|
||||
|
||||
复用 ``EvaluationSnapshot``(态势快照),产出 ``(system, user)`` 消息对,喂给
|
||||
模型网关。模型返回的 JSON 由 ``extract`` 模块解析。
|
||||
|
||||
枚举取值以 [内驱力生成器设计](../../docs/domain/core-domains.md) §5 与
|
||||
[数据库 schema](../../docs/contracts/contract-glossary.md) §22 为准。
|
||||
"""
|
||||
|
||||
from app.modules.big_cycle.prompts import EvaluationSnapshot
|
||||
|
||||
DRIVE_SOURCES = (
|
||||
"world_uncertainty",
|
||||
"memory_gap",
|
||||
"perception_change",
|
||||
"goal_tension",
|
||||
"reflection",
|
||||
"time_rhythm",
|
||||
)
|
||||
DRIVE_LEVELS = ("L0", "L1", "L2", "L3", "L4")
|
||||
|
||||
|
||||
def build_drive_extraction_messages(
|
||||
snapshot: EvaluationSnapshot,
|
||||
) -> tuple[str, str]:
|
||||
system = (
|
||||
"你是魂镜的内驱力生成器。基于本轮自我评估的态势快照,识别「匮乏感」——"
|
||||
"世界模型不确定性、记忆缺口、目标张力等——生成好奇问题与可审核的探索性次生目标。"
|
||||
"不生成已有确信答案的问题;不为满足好奇而越权(摄像头 / 麦克风 / 敏感读取仍需授权)。"
|
||||
f"source 取自:{', '.join(DRIVE_SOURCES)}。"
|
||||
f"risk_level 取自:{', '.join(DRIVE_LEVELS)}"
|
||||
"(探索风险等级,L4 必须人工确认)。"
|
||||
'以 JSON 数组返回,每个元素是一个好奇问题:{"question": str, "source": str, '
|
||||
'"expected_value": 0~1, "urgency": 0~1, "risk_level": str, '
|
||||
'"related_entities": [实体名], "subgoals": [{"title": str, "description": str, '
|
||||
'"expected_information_gain": 0~1, "interruption_cost": 0~1, "risk_level": str}]}。'
|
||||
"subgoals 为该好奇问题转化的可执行探索目标,可空数组。"
|
||||
"仅在态势确有缺口时生成,宁缺毋滥;无缺口返回 []。"
|
||||
"只返回 JSON 数组,不要解释或 markdown。"
|
||||
)
|
||||
user = (
|
||||
f"记忆总数:{snapshot.memory_count}\n"
|
||||
f"世界实体数:{snapshot.world_entity_count}\n"
|
||||
f"自我评估结论:{snapshot.conclusion or '(无明显态势)'}\n"
|
||||
f"当前人格摘要:\n{snapshot.personality_summary or '(尚未形成)'}\n"
|
||||
f"近期记忆摘要:\n{_format_memories(snapshot.recent_memories)}"
|
||||
)
|
||||
return system, user
|
||||
|
||||
|
||||
def _format_memories(memories: list[tuple[str, str]]) -> str:
|
||||
if not memories:
|
||||
return "(暂无)"
|
||||
lines = [f"[{idx}] {title}:{content}" for idx, (title, content) in enumerate(memories)]
|
||||
return "\n".join(lines)
|
||||
@@ -1,18 +0,0 @@
|
||||
"""演化候选领域纯逻辑。
|
||||
|
||||
本模块是演化系统(EvolutionCandidate)的纯函数域,禁止 IO。
|
||||
IO 操作由 ``services/evolution.py`` 或 ``application/big_cycle.py`` 负责编排。
|
||||
|
||||
当前内容:
|
||||
- ``policies`` — level 分类、自动应用策略、类型/作用域枚举
|
||||
"""
|
||||
|
||||
from app.modules.evolution.policies import AUTO_LEVELS, LEVELS, SCOPES, TYPES, should_auto_apply
|
||||
|
||||
__all__ = [
|
||||
"AUTO_LEVELS",
|
||||
"LEVELS",
|
||||
"SCOPES",
|
||||
"TYPES",
|
||||
"should_auto_apply",
|
||||
]
|
||||
@@ -1,57 +0,0 @@
|
||||
"""演化候选领域纯逻辑(level 分类、自动应用策略、类型/作用域枚举)。
|
||||
|
||||
本模块是演化候选相关常量与纯谓词的单一源,禁止 IO。
|
||||
枚举取值以 [契约字典](../../docs/contracts/contract-glossary.md) §12 为准。
|
||||
"""
|
||||
|
||||
from app.models.evolution import EvolutionCandidate
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Level 常量与自动应用策略
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# 半自动应用的 level 范围(L0-L3 写入程序记忆/人格权重等轻量参数,L4 核心变更需人工审核)
|
||||
AUTO_LEVELS: set[str] = {"L0", "L1", "L2", "L3"}
|
||||
|
||||
# 所有可选 level,按风险升序排列
|
||||
LEVELS: tuple[str, ...] = ("L0", "L1", "L2", "L3", "L4")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Type / Scope 枚举
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# 演化候选类型
|
||||
TYPES: tuple[str, ...] = (
|
||||
"personality",
|
||||
"personality_weights",
|
||||
"retrieval_strategy",
|
||||
"tool_affinity",
|
||||
"memory",
|
||||
"tool_strategy",
|
||||
"safety_rule",
|
||||
"world_model",
|
||||
"procedural",
|
||||
"mood",
|
||||
)
|
||||
|
||||
# 演化候选作用域
|
||||
SCOPES: tuple[str, ...] = (
|
||||
"character_scope",
|
||||
"user_scope",
|
||||
"task_scope",
|
||||
"global_scope",
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 策略谓词
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def should_auto_apply(candidate: EvolutionCandidate) -> bool:
|
||||
"""判断演化候选是否可半自动应用。
|
||||
|
||||
满足任一条件即可自动应用:
|
||||
1. level 在 AUTO_LEVELS 范围内(L0-L3 低风险调整)
|
||||
2. auto_apply_allowed 为 True(模型+系统共同判定允许自动应用)
|
||||
"""
|
||||
return candidate.level in AUTO_LEVELS or candidate.auto_apply_allowed
|
||||
Whitespace-only changes.
@@ -1,149 +0,0 @@
|
||||
"""灵核情绪状态机:纯计算(无 IO)。
|
||||
|
||||
消费辰正 ``time.*`` 事件(time.reunion / time.user_care / time.sentiment),
|
||||
派生 ``EmotionState``(mood + intensity)。intensity 向人格版本 ``emotion_baseline`` 指数衰减回归。
|
||||
设计见 ``docs/domain/core-domains.md`` §11。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
# v1 mood 词表(七种)。v2 路线图见设计 §11.6。
|
||||
MOODS: tuple[str, ...] = (
|
||||
"neutral",
|
||||
"pleased",
|
||||
"touched",
|
||||
"wistful",
|
||||
"concerned",
|
||||
"festive",
|
||||
"melancholy",
|
||||
)
|
||||
|
||||
MOOD_LABELS_ZH: dict[str, str] = {
|
||||
"neutral": "平静",
|
||||
"pleased": "愉悦",
|
||||
"touched": "感动",
|
||||
"wistful": "怅然怀念",
|
||||
"concerned": "关切",
|
||||
"festive": "喜庆",
|
||||
"melancholy": "惆怅",
|
||||
}
|
||||
|
||||
# 衰减半衰期(对话回合):intensity 向 baseline 走一半所需的回合数。
|
||||
DECAY_HALFLIFE_TURNS: float = 6.0
|
||||
# intensity 衰减至此阈值以下时,mood 回 baseline.mood。
|
||||
NEUTRAL_THRESHOLD: float = 0.15
|
||||
|
||||
# time.sentiment 无 payload 强度字段,使用固定中值。
|
||||
_SENTIMENT_DEFAULT_DELTA: float = 0.5
|
||||
|
||||
# time.sentiment trigger_type → mood(late_night → melancholy;其余 → wistful)。
|
||||
_SENTIMENT_TRIGGER_MOOD: dict[str, str] = {
|
||||
"late_night": "melancholy",
|
||||
}
|
||||
|
||||
|
||||
class EmotionState(BaseModel):
|
||||
"""情绪状态:离散 mood(``MOODS`` 枚举)+ intensity(0-1 标量)。"""
|
||||
|
||||
mood: str = "neutral"
|
||||
intensity: float = 0.2
|
||||
|
||||
|
||||
def parse_baseline(value: dict[str, Any] | str | None) -> EmotionState:
|
||||
"""从人格版本 ``emotion_baseline``(dict / JSON str / None)解析 baseline。
|
||||
|
||||
容错:非法 JSON / 非 dict / 未知 mood / 越界 intensity → 回退默认(neutral 0.2)。
|
||||
"""
|
||||
if value is None:
|
||||
return EmotionState()
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
value = json.loads(value)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return EmotionState()
|
||||
if not isinstance(value, dict):
|
||||
return EmotionState()
|
||||
mood = value.get("mood", "neutral")
|
||||
if mood not in MOODS:
|
||||
mood = "neutral"
|
||||
try:
|
||||
intensity = float(value.get("intensity", 0.2))
|
||||
except (TypeError, ValueError):
|
||||
intensity = 0.2
|
||||
intensity = max(0.0, min(1.0, intensity))
|
||||
return EmotionState(mood=mood, intensity=intensity)
|
||||
|
||||
|
||||
def map_signal_to_mood(event_type: str, payload: dict[str, Any]) -> str | None:
|
||||
"""辰正 time.* 事件 → mood 映射。无映射的事件类型返回 None。"""
|
||||
if event_type == "time.reunion":
|
||||
return "touched"
|
||||
if event_type == "time.user_care":
|
||||
if payload.get("care_kind") == "festival_lonely":
|
||||
return "melancholy"
|
||||
return "concerned"
|
||||
if event_type == "time.sentiment":
|
||||
trigger = payload.get("trigger_type", "")
|
||||
return _SENTIMENT_TRIGGER_MOOD.get(trigger, "wistful")
|
||||
return None
|
||||
|
||||
|
||||
def signal_intensity_delta(event_type: str, payload: dict[str, Any]) -> float:
|
||||
"""从事件 payload 提取 intensity delta(复用辰正既有强度,不重算)。"""
|
||||
if event_type == "time.reunion":
|
||||
return float(payload.get("reunion_intensity", 0.5))
|
||||
if event_type == "time.user_care":
|
||||
return float(payload.get("care_suggest", 0.5))
|
||||
if event_type == "time.sentiment":
|
||||
return _SENTIMENT_DEFAULT_DELTA
|
||||
return 0.0
|
||||
|
||||
|
||||
def apply_signal(
|
||||
state: EmotionState,
|
||||
event_type: str,
|
||||
payload: dict[str, Any],
|
||||
baseline: EmotionState,
|
||||
) -> EmotionState:
|
||||
"""事件驱动状态转移。
|
||||
|
||||
- mood 变化时:intensity = ``baseline.intensity + delta``(新情绪从基线起跳)。
|
||||
- mood 不变时:intensity = ``state.intensity + delta``(累积)。
|
||||
- clamp 至 [0, 1]。
|
||||
- 无映射的事件(``map_signal_to_mood`` 返回 None)原样返回 state。
|
||||
"""
|
||||
new_mood = map_signal_to_mood(event_type, payload)
|
||||
if new_mood is None:
|
||||
return state
|
||||
delta = signal_intensity_delta(event_type, payload)
|
||||
base = state.intensity if new_mood == state.mood else baseline.intensity
|
||||
intensity = max(0.0, min(1.0, base + delta))
|
||||
return EmotionState(mood=new_mood, intensity=round(intensity, 3))
|
||||
|
||||
|
||||
def decay(state: EmotionState, baseline: EmotionState, elapsed_turns: float) -> EmotionState:
|
||||
"""intensity 向 baseline 指数衰减;与 baseline 的差距收敛到阈值内时 mood 回 baseline.mood。
|
||||
|
||||
``elapsed_turns <= 0`` 时原样返回 state(无衰减)。
|
||||
判断用相对差距(``decayed - baseline.intensity``)而非绝对阈值——否则当
|
||||
baseline.intensity 本身 > 阈值时(如默认 neutral 0.2),mood 永远不会回归。
|
||||
"""
|
||||
if elapsed_turns <= 0:
|
||||
return state
|
||||
factor = 0.5 ** (elapsed_turns / DECAY_HALFLIFE_TURNS)
|
||||
decayed = baseline.intensity + (state.intensity - baseline.intensity) * factor
|
||||
if decayed - baseline.intensity <= NEUTRAL_THRESHOLD:
|
||||
return EmotionState(mood=baseline.mood, intensity=baseline.intensity)
|
||||
return EmotionState(mood=state.mood, intensity=round(decayed, 3))
|
||||
|
||||
|
||||
def to_prompt_text(state: EmotionState) -> str:
|
||||
"""注入对话 system prompt 的情绪摘要(中文 label + 强度百分比)。"""
|
||||
label = MOOD_LABELS_ZH.get(state.mood, state.mood)
|
||||
pct = round(state.intensity * 100)
|
||||
return f"[当前情绪] {label}(强度 {pct}%)"
|
||||
Reference in new issue
Block a user