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,96 +0,0 @@
|
||||
"""大周天演化候选抽取纯函数测试。"""
|
||||
|
||||
from app.modules.big_cycle.extract import (
|
||||
EvolutionCandidateDraft,
|
||||
parse_evolution_candidates,
|
||||
)
|
||||
|
||||
|
||||
def test_parse_standard_json_array() -> None:
|
||||
raw = (
|
||||
'[{"type":"personality","level":"L4","scope":"character_scope",'
|
||||
'"title":"调整身份表达","proposal":"更主动介绍自身能力边界",'
|
||||
'"delta":{"identity_profile":"新的身份描述"},"confidence":0.8,'
|
||||
'"benefit_score":0.7,"risk_score":0.3,"auto_apply_allowed":false,'
|
||||
'"evidence":[0,1]}]'
|
||||
)
|
||||
drafts = parse_evolution_candidates(raw)
|
||||
assert len(drafts) == 1
|
||||
d = drafts[0]
|
||||
assert d.type == "personality"
|
||||
assert d.level == "L4"
|
||||
assert d.scope == "character_scope"
|
||||
assert d.title == "调整身份表达"
|
||||
assert d.proposal == "更主动介绍自身能力边界"
|
||||
assert d.delta == {"identity_profile": "新的身份描述"}
|
||||
assert d.confidence == 0.8
|
||||
assert d.auto_apply_allowed is False
|
||||
assert d.source == "big_cycle"
|
||||
assert d.evidence == [0, 1]
|
||||
|
||||
|
||||
def test_parse_markdown_code_block() -> None:
|
||||
raw = (
|
||||
'```json\n[{"type":"procedural","level":"L2","scope":"task_scope",'
|
||||
'"title":"t","proposal":"p"}]\n```'
|
||||
)
|
||||
drafts = parse_evolution_candidates(raw)
|
||||
assert len(drafts) == 1
|
||||
assert drafts[0].type == "procedural"
|
||||
|
||||
|
||||
def test_parse_text_mixed_json() -> None:
|
||||
raw = (
|
||||
'候选如下:\n[{"type":"mood","level":"L1","scope":"character_scope",'
|
||||
'"title":"t","proposal":"p"}]\n以上。'
|
||||
)
|
||||
drafts = parse_evolution_candidates(raw)
|
||||
assert len(drafts) == 1
|
||||
|
||||
|
||||
def test_parse_invalid_json_returns_empty() -> None:
|
||||
assert parse_evolution_candidates("不是JSON") == []
|
||||
assert parse_evolution_candidates("[]") == []
|
||||
|
||||
|
||||
def test_skip_entries_missing_required_fields() -> None:
|
||||
raw = (
|
||||
'[{"type":"personality","title":"缺proposal"},'
|
||||
'{"title":"缺type","proposal":"p"},'
|
||||
'{"type":"x","proposal":"缺title"}]'
|
||||
)
|
||||
assert parse_evolution_candidates(raw) == []
|
||||
|
||||
|
||||
def test_defaults_use_safe_pessimistic_values() -> None:
|
||||
"""缺省时取最严格默认:level=L4(必须人工)、auto_apply_allowed=False。"""
|
||||
raw = '[{"type":"personality","title":"t","proposal":"p"}]'
|
||||
d = parse_evolution_candidates(raw)[0]
|
||||
assert d.level == "L4"
|
||||
assert d.scope == "character_scope"
|
||||
assert d.auto_apply_allowed is False
|
||||
assert d.confidence == 0.5
|
||||
assert d.benefit_score == 0.5
|
||||
assert d.risk_score == 0.5
|
||||
assert d.drift_score == 0.0
|
||||
assert d.delta == {}
|
||||
|
||||
|
||||
def test_clamp_and_default_scores() -> None:
|
||||
raw = (
|
||||
'[{"type":"personality","title":"t","proposal":"p",'
|
||||
'"confidence":1.5,"benefit_score":-0.2,"risk_score":"high"}]'
|
||||
)
|
||||
d = parse_evolution_candidates(raw)[0]
|
||||
assert d.confidence == 1.0
|
||||
assert d.benefit_score == 0.0
|
||||
assert d.risk_score == 0.5 # 非法值 → 默认
|
||||
|
||||
|
||||
def test_auto_apply_allowed_truthy() -> None:
|
||||
raw = (
|
||||
'[{"type":"procedural","level":"L2","title":"t","proposal":"p","auto_apply_allowed":true}]'
|
||||
)
|
||||
d = parse_evolution_candidates(raw)[0]
|
||||
assert d.auto_apply_allowed is True
|
||||
assert isinstance(d, EvolutionCandidateDraft)
|
||||
@@ -1,62 +0,0 @@
|
||||
"""大周天演化候选 prompt 构建纯函数测试。"""
|
||||
|
||||
from app.modules.big_cycle.prompts import (
|
||||
EvaluationSnapshot,
|
||||
build_evolution_extraction_messages,
|
||||
)
|
||||
|
||||
|
||||
def _snapshot() -> EvaluationSnapshot:
|
||||
return EvaluationSnapshot(
|
||||
character_id="character_x",
|
||||
memory_count=25,
|
||||
recent_memories=[("喜欢简洁回答", "用户偏好简洁"), ("来自北京", "用户所在地")],
|
||||
personality_summary="身份:魂镜;表达风格:温和",
|
||||
world_entity_count=3,
|
||||
conclusion="近期互动偏技术话题,表达可更简洁",
|
||||
)
|
||||
|
||||
|
||||
def test_messages_return_system_user_pair() -> None:
|
||||
system, user = build_evolution_extraction_messages(_snapshot())
|
||||
assert isinstance(system, str) and isinstance(user, str)
|
||||
assert system and user
|
||||
|
||||
|
||||
def test_system_describes_contract_fields() -> None:
|
||||
system, _ = build_evolution_extraction_messages(_snapshot())
|
||||
for token in (
|
||||
"type",
|
||||
"level",
|
||||
"scope",
|
||||
"proposal",
|
||||
"delta",
|
||||
"auto_apply_allowed",
|
||||
):
|
||||
assert token in system
|
||||
|
||||
|
||||
def test_system_lists_evolution_types() -> None:
|
||||
system, _ = build_evolution_extraction_messages(_snapshot())
|
||||
for t in ("personality", "procedural", "tool_strategy", "memory"):
|
||||
assert t in system
|
||||
|
||||
|
||||
def test_system_states_level_semantics() -> None:
|
||||
system, _ = build_evolution_extraction_messages(_snapshot())
|
||||
assert "L4" in system
|
||||
|
||||
|
||||
def test_user_includes_snapshot_signals() -> None:
|
||||
_, user = build_evolution_extraction_messages(_snapshot())
|
||||
assert "喜欢简洁回答" in user
|
||||
assert "身份:魂镜" in user
|
||||
assert "25" in user
|
||||
|
||||
|
||||
def test_user_handles_empty_personality() -> None:
|
||||
snap = _snapshot()
|
||||
snap.personality_summary = ""
|
||||
_, user = build_evolution_extraction_messages(snap)
|
||||
assert isinstance(user, str)
|
||||
assert len(user) > 0
|
||||
@@ -1,95 +0,0 @@
|
||||
"""大周天内驱力抽取纯函数测试。"""
|
||||
|
||||
from app.modules.drive.extract import (
|
||||
CuriosityDraft,
|
||||
SubgoalDraft,
|
||||
parse_curiosities,
|
||||
)
|
||||
|
||||
|
||||
def test_parse_standard_with_subgoals() -> None:
|
||||
raw = (
|
||||
'[{"question":"用户为何频繁提到项目X","source":"memory_gap",'
|
||||
'"expected_value":0.7,"urgency":0.5,"risk_level":"L2",'
|
||||
'"related_entities":["项目X"],'
|
||||
'"subgoals":[{"title":"询问项目X目标","description":"主动询问用户",'
|
||||
'"expected_information_gain":0.6,"interruption_cost":0.3,"risk_level":"L2"}]}]'
|
||||
)
|
||||
drafts = parse_curiosities(raw)
|
||||
assert len(drafts) == 1
|
||||
c = drafts[0]
|
||||
assert c.question == "用户为何频繁提到项目X"
|
||||
assert c.source == "memory_gap"
|
||||
assert c.expected_value == 0.7
|
||||
assert c.risk_level == "L2"
|
||||
assert c.related_entities == ["项目X"]
|
||||
assert len(c.subgoals) == 1
|
||||
sg = c.subgoals[0]
|
||||
assert sg.title == "询问项目X目标"
|
||||
assert sg.expected_information_gain == 0.6
|
||||
assert sg.interruption_cost == 0.3
|
||||
|
||||
|
||||
def test_parse_markdown_block() -> None:
|
||||
raw = '```json\n[{"question":"q"}]\n```'
|
||||
drafts = parse_curiosities(raw)
|
||||
assert len(drafts) == 1
|
||||
assert drafts[0].question == "q"
|
||||
|
||||
|
||||
def test_parse_invalid_returns_empty() -> None:
|
||||
assert parse_curiosities("不是JSON") == []
|
||||
assert parse_curiosities("[]") == []
|
||||
|
||||
|
||||
def test_skip_missing_question() -> None:
|
||||
raw = '[{"source":"memory_gap"},{"question":"q"}]'
|
||||
drafts = parse_curiosities(raw)
|
||||
assert len(drafts) == 1
|
||||
assert drafts[0].question == "q"
|
||||
|
||||
|
||||
def test_defaults_use_safe_pessimistic_values() -> None:
|
||||
"""缺省取最严格默认:risk_level=L4、source=reflection。"""
|
||||
raw = '[{"question":"q"}]'
|
||||
c = parse_curiosities(raw)[0]
|
||||
assert c.source == "reflection"
|
||||
assert c.risk_level == "L4"
|
||||
assert c.expected_value == 0.5
|
||||
assert c.urgency == 0.5
|
||||
assert c.related_entities == []
|
||||
assert c.subgoals == []
|
||||
|
||||
|
||||
def test_clamp_values() -> None:
|
||||
raw = '[{"question":"q","expected_value":1.5,"urgency":-0.2}]'
|
||||
c = parse_curiosities(raw)[0]
|
||||
assert c.expected_value == 1.0
|
||||
assert c.urgency == 0.0
|
||||
|
||||
|
||||
def test_invalid_source_defaults() -> None:
|
||||
raw = '[{"question":"q","source":"unknown_source"}]'
|
||||
assert parse_curiosities(raw)[0].source == "reflection"
|
||||
|
||||
|
||||
def test_invalid_risk_level_defaults() -> None:
|
||||
raw = '[{"question":"q","risk_level":"L9"}]'
|
||||
assert parse_curiosities(raw)[0].risk_level == "L4"
|
||||
|
||||
|
||||
def test_subgoal_defaults_and_types() -> None:
|
||||
raw = '[{"question":"q","subgoals":[{"title":"t","description":"d"}]}]'
|
||||
c = parse_curiosities(raw)[0]
|
||||
assert isinstance(c, CuriosityDraft)
|
||||
sg = c.subgoals[0]
|
||||
assert isinstance(sg, SubgoalDraft)
|
||||
assert sg.risk_level == "L4" # 安全默认
|
||||
assert sg.expected_information_gain == 0.5
|
||||
assert sg.interruption_cost == 0.5
|
||||
|
||||
|
||||
def test_skip_subgoal_missing_title_or_description() -> None:
|
||||
raw = '[{"question":"q","subgoals":[{"title":"t"},{"description":"d"},{"title":"t","description":"d"}]}]'
|
||||
c = parse_curiosities(raw)[0]
|
||||
assert len(c.subgoals) == 1 # 只有完整的保留
|
||||
@@ -1,47 +0,0 @@
|
||||
"""大周天内驱力 prompt 构建纯函数测试。"""
|
||||
|
||||
from app.modules.big_cycle.prompts import EvaluationSnapshot
|
||||
from app.modules.drive.prompts import build_drive_extraction_messages
|
||||
|
||||
|
||||
def _snapshot() -> EvaluationSnapshot:
|
||||
return EvaluationSnapshot(
|
||||
character_id="character_x",
|
||||
memory_count=15,
|
||||
recent_memories=[("用户偏好简洁", "用户偏好简洁直接的回答")],
|
||||
personality_summary="身份:魂镜",
|
||||
world_entity_count=4,
|
||||
conclusion="近期对话偏技术,存在项目背景缺口",
|
||||
)
|
||||
|
||||
|
||||
def test_messages_return_system_user_pair() -> None:
|
||||
system, user = build_drive_extraction_messages(_snapshot())
|
||||
assert isinstance(system, str) and isinstance(user, str)
|
||||
assert system and user
|
||||
|
||||
|
||||
def test_system_describes_contract_fields() -> None:
|
||||
system, _ = build_drive_extraction_messages(_snapshot())
|
||||
for token in (
|
||||
"question",
|
||||
"source",
|
||||
"expected_value",
|
||||
"urgency",
|
||||
"risk_level",
|
||||
"subgoals",
|
||||
):
|
||||
assert token in system
|
||||
|
||||
|
||||
def test_system_lists_sources() -> None:
|
||||
system, _ = build_drive_extraction_messages(_snapshot())
|
||||
for src in ("world_uncertainty", "memory_gap", "reflection", "goal_tension"):
|
||||
assert src in system
|
||||
|
||||
|
||||
def test_user_includes_snapshot_signals() -> None:
|
||||
_, user = build_drive_extraction_messages(_snapshot())
|
||||
assert "用户偏好简洁" in user
|
||||
assert "身份:魂镜" in user
|
||||
assert "15" in user
|
||||
@@ -1,182 +0,0 @@
|
||||
"""灵核情绪状态机纯计算测试(modules/personality/emotion.py)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from app.modules.personality.emotion import (
|
||||
MOODS,
|
||||
EmotionState,
|
||||
apply_signal,
|
||||
decay,
|
||||
map_signal_to_mood,
|
||||
parse_baseline,
|
||||
signal_intensity_delta,
|
||||
to_prompt_text,
|
||||
)
|
||||
|
||||
|
||||
def _baseline(mood: str = "neutral", intensity: float = 0.2) -> EmotionState:
|
||||
return EmotionState(mood=mood, intensity=intensity)
|
||||
|
||||
|
||||
# ---- map_signal_to_mood ----
|
||||
|
||||
|
||||
def test_map_reunion_to_touched() -> None:
|
||||
assert map_signal_to_mood("time.reunion", {"reunion_intensity": 0.8}) == "touched"
|
||||
|
||||
|
||||
def test_map_user_care_default_to_concerned() -> None:
|
||||
assert map_signal_to_mood("time.user_care", {"care_kind": "late_night"}) == "concerned"
|
||||
|
||||
|
||||
def test_map_user_care_festival_lonely_to_melancholy() -> None:
|
||||
assert map_signal_to_mood("time.user_care", {"care_kind": "festival_lonely"}) == "melancholy"
|
||||
|
||||
|
||||
def test_map_sentiment_default_to_wistful() -> None:
|
||||
assert map_signal_to_mood("time.sentiment", {"trigger_type": "reunion_memory"}) == "wistful"
|
||||
|
||||
|
||||
def test_map_sentiment_late_night_to_melancholy() -> None:
|
||||
assert map_signal_to_mood("time.sentiment", {"trigger_type": "late_night"}) == "melancholy"
|
||||
|
||||
|
||||
def test_map_unknown_event_returns_none() -> None:
|
||||
assert map_signal_to_mood("tool.started", {}) is None
|
||||
|
||||
|
||||
# ---- signal_intensity_delta ----
|
||||
|
||||
|
||||
def test_delta_reunion_uses_reunion_intensity() -> None:
|
||||
assert signal_intensity_delta("time.reunion", {"reunion_intensity": 0.7}) == 0.7
|
||||
|
||||
|
||||
def test_delta_user_care_uses_care_suggest() -> None:
|
||||
assert signal_intensity_delta("time.user_care", {"care_suggest": 0.6}) == 0.6
|
||||
|
||||
|
||||
def test_delta_sentiment_default_05() -> None:
|
||||
assert signal_intensity_delta("time.sentiment", {}) == 0.5
|
||||
|
||||
|
||||
def test_delta_unknown_event_zero() -> None:
|
||||
assert signal_intensity_delta("tool.started", {}) == 0.0
|
||||
|
||||
|
||||
# ---- apply_signal ----
|
||||
|
||||
|
||||
def test_apply_signal_changes_mood_and_sets_intensity_from_baseline() -> None:
|
||||
state = _baseline() # neutral 0.2
|
||||
new = apply_signal(state, "time.reunion", {"reunion_intensity": 0.6}, _baseline())
|
||||
assert new.mood == "touched"
|
||||
assert new.intensity == 0.8 # 0.2 baseline + 0.6 delta
|
||||
|
||||
|
||||
def test_apply_signal_same_mood_accumulates() -> None:
|
||||
state = EmotionState(mood="touched", intensity=0.5)
|
||||
new = apply_signal(state, "time.reunion", {"reunion_intensity": 0.3}, _baseline())
|
||||
assert new.mood == "touched"
|
||||
assert new.intensity == 0.8 # 0.5 current + 0.3 delta
|
||||
|
||||
|
||||
def test_apply_signal_clamps_to_1() -> None:
|
||||
state = EmotionState(mood="touched", intensity=0.9)
|
||||
new = apply_signal(state, "time.reunion", {"reunion_intensity": 0.5}, _baseline())
|
||||
assert new.mood == "touched"
|
||||
assert new.intensity == 1.0
|
||||
|
||||
|
||||
def test_apply_signal_unknown_event_returns_unchanged() -> None:
|
||||
state = EmotionState(mood="pleased", intensity=0.5)
|
||||
new = apply_signal(state, "tool.started", {}, _baseline())
|
||||
assert new == state
|
||||
|
||||
|
||||
# ---- decay ----
|
||||
|
||||
|
||||
def test_decay_toward_baseline_half_life() -> None:
|
||||
state = EmotionState(mood="touched", intensity=0.8)
|
||||
baseline = _baseline() # neutral 0.2
|
||||
# 半衰期 6 回合,elapsed 6 → intensity 向 baseline 走一半
|
||||
decayed = decay(state, baseline, elapsed_turns=6.0)
|
||||
assert decayed.mood == "touched"
|
||||
assert abs(decayed.intensity - 0.5) < 0.01 # 0.2 + (0.8-0.2)*0.5 = 0.5
|
||||
|
||||
|
||||
def test_decay_below_threshold_returns_baseline_mood() -> None:
|
||||
state = EmotionState(mood="touched", intensity=0.3)
|
||||
baseline = _baseline()
|
||||
# elapsed 大 → intensity 衰减到 < 0.15 → mood 回 baseline
|
||||
decayed = decay(state, baseline, elapsed_turns=30.0)
|
||||
assert decayed.mood == baseline.mood
|
||||
assert decayed.intensity == baseline.intensity
|
||||
|
||||
|
||||
def test_decay_zero_elapsed_returns_unchanged() -> None:
|
||||
state = EmotionState(mood="touched", intensity=0.8)
|
||||
baseline = _baseline()
|
||||
assert decay(state, baseline, elapsed_turns=0) == state
|
||||
|
||||
|
||||
# ---- parse_baseline ----
|
||||
|
||||
|
||||
def test_parse_baseline_from_dict() -> None:
|
||||
assert parse_baseline({"mood": "neutral", "intensity": 0.3}) == EmotionState(
|
||||
mood="neutral", intensity=0.3
|
||||
)
|
||||
|
||||
|
||||
def test_parse_baseline_from_valid_json() -> None:
|
||||
assert parse_baseline('{"mood": "pleased", "intensity": 0.4}') == EmotionState(
|
||||
mood="pleased", intensity=0.4
|
||||
)
|
||||
|
||||
|
||||
def test_parse_baseline_invalid_json_returns_neutral_default() -> None:
|
||||
assert parse_baseline("not json") == EmotionState()
|
||||
|
||||
|
||||
def test_parse_baseline_none_returns_neutral_default() -> None:
|
||||
assert parse_baseline(None) == EmotionState()
|
||||
|
||||
|
||||
def test_parse_baseline_clamps_intensity() -> None:
|
||||
assert parse_baseline({"mood": "neutral", "intensity": 1.5}).intensity == 1.0
|
||||
assert parse_baseline({"mood": "neutral", "intensity": -0.2}).intensity == 0.0
|
||||
|
||||
|
||||
def test_parse_baseline_unknown_mood_falls_back_neutral() -> None:
|
||||
assert parse_baseline({"mood": "unknown_mood", "intensity": 0.5}).mood == "neutral"
|
||||
|
||||
|
||||
# ---- to_prompt_text ----
|
||||
|
||||
|
||||
def test_to_prompt_text_includes_chinese_label_and_pct() -> None:
|
||||
text = to_prompt_text(EmotionState(mood="touched", intensity=0.7))
|
||||
assert "感动" in text
|
||||
assert "70" in text
|
||||
|
||||
|
||||
def test_to_prompt_text_unknown_mood_uses_raw() -> None:
|
||||
text = to_prompt_text(EmotionState(mood="custom", intensity=0.5))
|
||||
assert "custom" in text
|
||||
|
||||
|
||||
# ---- MOODS ----
|
||||
|
||||
|
||||
def test_moods_contains_seven() -> None:
|
||||
assert set(MOODS) == {
|
||||
"neutral",
|
||||
"pleased",
|
||||
"touched",
|
||||
"wistful",
|
||||
"concerned",
|
||||
"festive",
|
||||
"melancholy",
|
||||
}
|
||||
Reference in new issue
Block a user