liquid-loop 0.5.3__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: liquid-loop
3
- Version: 0.5.3
3
+ Version: 0.6.0
4
4
  Summary: Self-Organizing Cognitive Memory for AI Agents — Liquid Loop theory implementation
5
5
  Author: fishbook0001
6
6
  Maintainer: fishbook0001
@@ -1,9 +1,9 @@
1
- """Liquid Loop — Workspace Cognitive Runtime v0.5.1 (CPE-fused, proactive)"""
2
- __version__ = "0.5.2"
1
+ """Liquid Loop — Workspace Cognitive Runtime v0.6.0 (CPE-fused, proactive)"""
2
+ __version__ = "0.6.0"
3
3
 
4
4
  from .workspace import (
5
5
  WorkspaceState, Anchor, Evidence, Memory, Conflict,
6
6
  AuditChain, CPERegularizer, SelfRefineEngine,
7
7
  )
8
8
  from .storage import load, save
9
- from .entropy import calculate, calculate_detail, calculate as calculate_entropy
9
+ from .entropy import calculate, calculate_detail, calculate as calculate_entropy
@@ -80,7 +80,7 @@ def init():
80
80
  if s.anchors or s.evidences:
81
81
  click.echo("工作区已初始化,无需重复操作。")
82
82
  return
83
- s.version = "0.5.2"
83
+ s.version = "0.5.1"
84
84
  _save(s)
85
85
  click.echo(f"✓ Liquid Loop v0.5.1 工作区已初始化: {WORKSPACE}/.liquid/")
86
86
 
@@ -520,4 +520,56 @@ def cpe_status():
520
520
 
521
521
 
522
522
  if __name__ == "__main__":
523
- main()
523
+ main()
524
+
525
+ @cli.command()
526
+ @click.argument("content")
527
+ @click.option("--evidence-ids", help="证据ID列表,逗号分隔")
528
+ def memory_add(content: str, evidence_ids: str):
529
+ """添加一条记忆结晶"""
530
+ state = load_state()
531
+ eids = [eid.strip() for eid in evidence_ids.split(",")] if evidence_ids else None
532
+ state.add_memory(content, eids)
533
+ save_state(state)
534
+ click.echo(f"Memory added: {content[:50]}...")
535
+
536
+ @cli.command()
537
+ def memory_list():
538
+ """列出所有记忆结晶"""
539
+ state = load_state()
540
+ if not state.memories:
541
+ click.echo("No memories yet")
542
+ return
543
+ for m in state.memories:
544
+ click.echo(f" {m.id[:8]} | conf={m.confidence:.2f} | {m.content[:60]}...")
545
+
546
+ @cli.command()
547
+ @click.argument("anchor_id")
548
+ def conflict_resolve(anchor_id: str):
549
+ """标记冲突为已解决(移除该锚点的冲突记录)"""
550
+ state = load_state()
551
+ before = len(state.conflicts)
552
+ state.conflicts = [c for c in state.conflicts if c.anchor_a != anchor_id]
553
+ after = len(state.conflicts)
554
+ save_state(state)
555
+ click.echo(f"Resolved {before - after} conflict(s) for anchor {anchor_id}")
556
+
557
+ @cli.command()
558
+ @click.argument("anchor_id")
559
+ @click.argument("description")
560
+ def anchor_describe(anchor_id: str, description: str):
561
+ """手动设置锚点描述"""
562
+ state = load_state()
563
+ anchor = next((a for a in state.anchors if a.id == anchor_id or a.name == anchor_id), None)
564
+ if not anchor:
565
+ click.echo(f"Anchor not found: {anchor_id}")
566
+ return
567
+ anchor.description = description
568
+ save_state(state)
569
+ click.echo(f"Anchor '{anchor.name}' description updated")
570
+
571
+ @cli.command()
572
+ def version():
573
+ """显示版本号"""
574
+ from . import __version__
575
+ click.echo(f"liquid-loop {__version__}")
@@ -2,6 +2,18 @@ from datetime import datetime, timezone, timedelta
2
2
  from .workspace import WorkspaceState
3
3
 
4
4
 
5
+ from contextlib import contextmanager
6
+
7
+ @contextmanager
8
+ def temp_attr(obj, attr: str, new_val):
9
+ """临时修改对象属性,退出时自动还原(异常安全)"""
10
+ old_val = getattr(obj, attr)
11
+ setattr(obj, attr, new_val)
12
+ try:
13
+ yield
14
+ finally:
15
+ setattr(obj, attr, old_val)
16
+
5
17
  def anchor_drift(state: WorkspaceState) -> float:
6
18
  """所有 Anchor 的 stability 偏离 1.0 的均值。0=全稳, 1=全漂"""
7
19
  if not state.anchors:
@@ -79,31 +91,25 @@ def strength_entropy(state: WorkspaceState) -> float:
79
91
  def retrospective_decay_entropy(state: WorkspaceState) -> float:
80
92
  """回顾性衰退熵。0=无衰退, 1=大规模衰退(CPE §1: Retrospective decay)
81
93
 
82
- 纯函数快照,不改 state。每锚点计算最新 vs 次新证据的 value_score 变化比。
94
+ 语义对齐 CPE 定义:新知识(证据)加入后,旧锚点基线能力(value_score)的下降比例。
95
+ 维护每个锚点的 baseline_value_score(首次结晶/人工设定时的基线),对比当前值。
83
96
  """
84
97
  if not state.anchors or not state.evidences:
85
98
  return 0.0
86
99
  decay_scores = []
87
100
  for a in state.anchors:
88
- evs = sorted(
89
- [e for e in state.evidences if e.anchor_id == a.id],
90
- key=lambda x: x.timestamp, reverse=True
91
- )
101
+ evs = [e for e in state.evidences if e.anchor_id == a.id]
92
102
  if len(evs) < 2:
93
103
  continue
94
- # 快照原始值
95
- orig_score = a.value_score
96
- orig_strength = a.anchor_strength
97
- # 最新证据时的 score
98
- new_score = a.decay_value(evidence_count=len(evs))
99
- new_score_val = a.value_score
100
- # 少一条证据时的 score(传参模式,不依赖 state 缓存)
101
- old_score = a.decay_value(evidence_count=len(evs) - 1)
102
- # 还原 state
103
- a.value_score = orig_score
104
- a.anchor_strength = orig_strength
105
- if old_score > 0:
106
- ratio = max(0, 1 - new_score_val / old_score)
104
+ # 基线:锚点创建时或首次结晶时的 value_score(若无记录,用当前值的 1.1 倍估算)
105
+ baseline = getattr(a, "baseline_value_score", None)
106
+ if baseline is None:
107
+ baseline = a.value_score * 1.1
108
+ a.baseline_value_score = baseline
109
+ # 当前衰减后的价值
110
+ current = a.decay_value(evidence_count=len(evs))
111
+ if baseline > 0:
112
+ ratio = max(0, 1 - current / baseline)
107
113
  decay_scores.append(ratio)
108
114
  return sum(decay_scores) / len(decay_scores) if decay_scores else 0.0
109
115
 
@@ -111,7 +117,8 @@ def retrospective_decay_entropy(state: WorkspaceState) -> float:
111
117
  def behavioral_drift_entropy(state: WorkspaceState) -> float:
112
118
  """策略漂移熵。0=无漂移, 1=剧烈漂移(CPE §1: Behavioral policy drift)
113
119
 
114
- 纯函数快照,不改 state。测量锚点 stability 在最近一次更新中的变化幅度。
120
+ 语义:锚点锚定强度在最近一次证据更新前后的变化幅度。
121
+ 使用上下文管理器保证不修改 state。
115
122
  """
116
123
  if not state.anchors:
117
124
  return 0.0
@@ -121,16 +128,12 @@ def behavioral_drift_entropy(state: WorkspaceState) -> float:
121
128
  if len(evs) < 2:
122
129
  continue
123
130
  evs_sorted = sorted(evs, key=lambda x: x.timestamp, reverse=True)
124
- # 快照原始值
125
- orig_strength = a.anchor_strength
126
- # 模拟有/无最新证据时的 stability 差
127
- old_count = len(evs_sorted) - 1
128
- new_count = len(evs_sorted)
129
- old_strength = a.recalc_strength(old_count)
130
- new_strength = a.recalc_strength(new_count)
131
- drifts.append(abs(new_strength - old_strength))
132
- # 还原
133
- a.anchor_strength = orig_strength
131
+ with temp_attr(a, "anchor_strength", a.anchor_strength):
132
+ old_count = len(evs_sorted) - 1
133
+ new_count = len(evs_sorted)
134
+ old_strength = a.recalc_strength(old_count)
135
+ new_strength = a.recalc_strength(new_count)
136
+ drifts.append(abs(new_strength - old_strength))
134
137
  return sum(drifts) / len(drifts) if drifts else 0.0
135
138
 
136
139
 
@@ -152,7 +155,7 @@ def generalization_erosion_entropy(state: WorkspaceState) -> float:
152
155
  for j in range(i + 1, len(evs)):
153
156
  if evs[i].content and evs[j].content:
154
157
  from .workspace import _keyword_overlap
155
- overlaps.append(_keyword_overlap(evs[i].content, evs[j].content))
158
+ overlaps.append(_keyword_overlap(evs[i].content, evs[j].content, state.overlap_cache))
156
159
  if overlaps:
157
160
  avg_overlap = sum(overlaps) / len(overlaps)
158
161
  # 重叠度越低 → 崩塌熵越高
@@ -23,6 +23,18 @@ def get_audit_chain(workspace_root: Path) -> AuditChain:
23
23
  """获取审计链实例"""
24
24
  return AuditChain(str(_ensure_dir(workspace_root) / AUDIT_FILE))
25
25
 
26
+ import fcntl
27
+ from contextlib import contextmanager
28
+
29
+ @contextmanager
30
+ def _file_lock(filepath: str, mode: str = "w"):
31
+ """跨平台文件锁(fcntl,Linux/macOS)"""
32
+ with open(filepath, mode) as f:
33
+ try:
34
+ fcntl.flock(f, fcntl.LOCK_EX)
35
+ yield f
36
+ finally:
37
+ fcntl.flock(f, fcntl.LOCK_UN)
26
38
 
27
39
  def load(workspace_root: Path) -> WorkspaceState:
28
40
  path = _ensure_dir(workspace_root) / STATE_FILE
@@ -2,6 +2,7 @@ from dataclasses import dataclass, field
2
2
  from datetime import datetime, timezone
3
3
  from typing import Optional, List, Dict, Any
4
4
  import uuid
5
+ from pathlib import Path
5
6
  import hashlib
6
7
  import json
7
8
  import os
@@ -187,6 +188,18 @@ class StateSnapshot:
187
188
  conflict_count: int = 0
188
189
 
189
190
 
191
+ def _get_version() -> str:
192
+ """从 pyproject.toml 读取版本号"""
193
+ try:
194
+ import tomllib
195
+ except ImportError:
196
+ import tomli as tomllib
197
+ pyproject = Path(__file__).parent.parent / "pyproject.toml"
198
+ if pyproject.exists():
199
+ data = tomllib.loads(pyproject.read_text())
200
+ return data.get("project", {}).get("version", "0.5.4")
201
+ return "0.5.4"
202
+
190
203
  @dataclass
191
204
  class WorkspaceState:
192
205
  anchors: list[Anchor] = field(default_factory=list)
@@ -197,7 +210,7 @@ class WorkspaceState:
197
210
  relations: list[AnchorRelation] = field(default_factory=list)
198
211
  audit_chain_hash: str = "genesis"
199
212
  audit_prev_hash: str = ""
200
- version: str = "0.5.2"
213
+ version: str = field(default_factory=_get_version)
201
214
  updated_at: str = field(default_factory=now)
202
215
  # 【v0.4.0】后向自进化状态(借鉴 MemMA 原位自进化)
203
216
  self_refine_probes: list[dict] = field(default_factory=list)
@@ -208,6 +221,7 @@ class WorkspaceState:
208
221
  blocked_evidences: list[str] = field(default_factory=list) # 被正则化拦截的证据ID
209
222
  cpe_erosion_warnings: list[dict] = field(default_factory=list) # 能力侵蚀告警
210
223
  cpe_regularization_count: int = 0 # 累计正则化干预次数
224
+ overlap_cache: dict = field(default_factory=dict) # 缓存关键词重叠度
211
225
 
212
226
  # ── API 层:add_anchor / add_evidence(让 README 示例能跑通)──
213
227
 
@@ -252,10 +266,11 @@ class WorkspaceState:
252
266
  return e
253
267
 
254
268
  def _on_evidence_added(self, anchor_id: str):
255
- """证据添加后的统一后处理:衰减→成核→稳定性刷新"""
269
+ """证据添加后的统一后处理:衰减→成核→稳定性刷新→冲突检测"""
256
270
  self._decay_anchor(anchor_id)
257
271
  self._nucleate(anchor_id)
258
272
  self._recalc_anchor(anchor_id)
273
+ self._detect_conflicts(anchor_id)
259
274
 
260
275
  def _decay_anchor(self, anchor_id: str):
261
276
  for e in self.evidences:
@@ -280,6 +295,54 @@ class WorkspaceState:
280
295
  evidence_ids=evidence_ids,
281
296
  confidence=confidence,
282
297
  ))
298
+ # 成核后触发自动描述回流(仅当描述为空)
299
+ self._auto_describe_anchor(anchor_id)
300
+
301
+ def _auto_describe_anchor(self, anchor_id: str):
302
+ """高置信结晶自动回填锚点空描述(尊重人工设定)"""
303
+ anchor = next((a for a in self.anchors if a.id == anchor_id), None)
304
+ if not anchor or anchor.description:
305
+ return
306
+ crystals = [m for m in self.memories if any(eid in [e.id for e in self.evidences if e.anchor_id == anchor_id] for eid in m.evidence_ids)]
307
+ if crystals:
308
+ best = max(crystals, key=lambda m: m.confidence)
309
+ if best.confidence >= 0.8:
310
+ anchor.description = best.content
311
+
312
+ def _detect_conflicts(self, anchor_id: str):
313
+ """群内自洽检测:同锚点证据一致性过低 -> 生成 Conflict 记录并降锚点稳定性。
314
+
315
+ 阈值从环境变量 LIQUID_CONFLICT_THRESHOLD 读取(默认 0.2)。
316
+ 复用 CPE 泛化防线思路:证据间平均关键词重叠 < threshold 且 >= 3 条 -> 潜在冲突/漂移。
317
+ 保守处理:只标记'需人工确认',不武断判相反(避免过度工程化误报)。
318
+ 使用 overlap_cache 避免重复计算。
319
+ """
320
+ import os
321
+ conflict_threshold = float(os.environ.get('LIQUID_CONFLICT_THRESHOLD', '0.2'))
322
+
323
+ group = [e for e in self.evidences if e.anchor_id == anchor_id]
324
+ if len(group) < 3:
325
+ return
326
+ overlaps = []
327
+ for i in range(len(group)):
328
+ for j in range(i + 1, len(group)):
329
+ if group[i].content and group[j].content:
330
+ overlaps.append(_keyword_overlap(group[i].content, group[j].content, self.overlap_cache))
331
+ if not overlaps:
332
+ return
333
+ avg = sum(overlaps) / len(overlaps)
334
+ if avg < conflict_threshold:
335
+ anchor = next((a for a in self.anchors if a.id == anchor_id), None)
336
+ if any(c.anchor_a == anchor_id for c in self.conflicts):
337
+ return
338
+ self.conflicts.append(Conflict(
339
+ anchor_a=anchor_id,
340
+ description=f"证据间平均一致度 {avg:.2f}(<{conflict_threshold}),存在潜在冲突/漂移,需人工确认",
341
+ severity=round(1.0 - avg, 2),
342
+ ))
343
+ if anchor:
344
+ anchor.stability = max(0.1, anchor.stability * 0.9)
345
+
283
346
 
284
347
  def _recalc_anchor(self, anchor_id: str):
285
348
  group = [e for e in self.evidences if e.anchor_id == anchor_id]
@@ -360,7 +423,6 @@ class CPERegularizer:
360
423
  "max_overlap": round(max_overlap, 3),
361
424
  "avg_overlap": round(avg_overlap, 3),
362
425
  "drift_score": round(drift_score, 3),
363
- "protection_weight": round(protection_weight, 3),
364
426
  "existing_evidence_count": len(evs),
365
427
  "duplicate_of": duplicate_of,
366
428
  }
@@ -590,12 +652,24 @@ def _tokenize(text: str) -> List[str]:
590
652
  return tokens
591
653
 
592
654
 
593
- def _keyword_overlap(a: str, b: str) -> float:
594
- """关键词重叠度(替代 embedding cosine)"""
595
- sa, sb = set(_tokenize(a)), set(_tokenize(b))
596
- if not sa or not sb:
655
+ def _keyword_overlap(a: str, b: str, cache: dict | None = None) -> float:
656
+ """关键词重叠度(Jaccard 系数),支持缓存"""
657
+ if not a or not b:
658
+ return 0.0
659
+ if cache is not None:
660
+ import hashlib
661
+ key = tuple(sorted([hashlib.md5(a.encode()).hexdigest()[:8], hashlib.md5(b.encode()).hexdigest()[:8]]))
662
+ if key in cache:
663
+ return cache[key]
664
+ import re
665
+ tokens1 = set(re.findall(r"[一-鿿]|[a-zA-Z0-9]+", a.lower()))
666
+ tokens2 = set(re.findall(r"[一-鿿]|[a-zA-Z0-9]+", b.lower()))
667
+ if not tokens1 or not tokens2:
597
668
  return 0.0
598
- return len(sa & sb) / len(sa | sb)
669
+ result = len(tokens1 & tokens2) / len(tokens1 | tokens2)
670
+ if cache is not None:
671
+ cache[key] = result
672
+ return result
599
673
 
600
674
 
601
675
  def _judge_answer(gold: str, answer: str) -> bool:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: liquid-loop
3
- Version: 0.5.3
3
+ Version: 0.6.0
4
4
  Summary: Self-Organizing Cognitive Memory for AI Agents — Liquid Loop theory implementation
5
5
  Author: fishbook0001
6
6
  Maintainer: fishbook0001
@@ -12,4 +12,7 @@ liquid_loop.egg-info/SOURCES.txt
12
12
  liquid_loop.egg-info/dependency_links.txt
13
13
  liquid_loop.egg-info/entry_points.txt
14
14
  liquid_loop.egg-info/requires.txt
15
- liquid_loop.egg-info/top_level.txt
15
+ liquid_loop.egg-info/top_level.txt
16
+ tests/test_entropy.py
17
+ tests/test_self_evolve.py
18
+ tests/test_workspace.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "liquid-loop"
7
- version = "0.5.3"
7
+ version = "0.6.0"
8
8
  description = "Self-Organizing Cognitive Memory for AI Agents — Liquid Loop theory implementation"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -0,0 +1,56 @@
1
+ import pytest
2
+ from liquid_loop.workspace import WorkspaceState, Anchor, Evidence
3
+ from liquid_loop.entropy import (
4
+ calculate, calculate_detail,
5
+ anchor_drift, conflict_density, evidence_fragmentation,
6
+ activity_gap, value_decay_entropy, strength_entropy,
7
+ retrospective_decay_entropy, behavioral_drift_entropy,
8
+ generalization_erosion_entropy
9
+ )
10
+
11
+ def test_calculate_returns_float():
12
+ state = WorkspaceState()
13
+ a = state.add_anchor('test')
14
+ state.add_evidence(a.id, 'evidence 1')
15
+ ent = calculate(state)
16
+ assert isinstance(ent, float)
17
+ assert 0.0 <= ent <= 1.0
18
+
19
+ def test_calculate_detail_has_all_keys():
20
+ state = WorkspaceState()
21
+ a = state.add_anchor('test')
22
+ state.add_evidence(a.id, 'evidence 1')
23
+ detail = calculate_detail(state)
24
+ assert 'combined' in detail
25
+ assert 'anchor_drift' in detail
26
+ assert 'cpe_retrospective_decay' in detail
27
+ assert 'cpe_behavioral_drift' in detail
28
+ assert 'cpe_generalization_erosion' in detail
29
+
30
+ def test_anchor_drift():
31
+ state = WorkspaceState()
32
+ a = state.add_anchor('test')
33
+ state.add_evidence(a.id, 'evidence 1')
34
+ drift = anchor_drift(state)
35
+ assert 0.0 <= drift <= 1.0
36
+
37
+ def test_conflict_density():
38
+ state = WorkspaceState()
39
+ a = state.add_anchor('test')
40
+ state.add_evidence(a.id, 'evidence 1')
41
+ density = conflict_density(state)
42
+ assert 0.0 <= density <= 1.0
43
+
44
+ def test_evidence_fragmentation():
45
+ state = WorkspaceState()
46
+ a = state.add_anchor('test')
47
+ state.add_evidence(a.id, 'evidence 1')
48
+ frag = evidence_fragmentation(state)
49
+ assert 0.0 <= frag <= 1.0
50
+
51
+ def test_activity_gap():
52
+ state = WorkspaceState()
53
+ a = state.add_anchor('test')
54
+ state.add_evidence(a.id, 'evidence 1')
55
+ gap = activity_gap(state)
56
+ assert 0.0 <= gap <= 1.0
@@ -0,0 +1,68 @@
1
+ """锚点自进化补强 + 回归测试(v0.5.3)
2
+
3
+ 覆盖:
4
+ 1. CPE MERGE 分支不再抛 NameError(回归)
5
+ 2. 成核回流:高置信结晶自动回填锚点空描述
6
+ 3. 群内自洽:低一致性证据自动生成 Conflict
7
+ """
8
+ from liquid_loop.workspace import WorkspaceState, CPERegularizer
9
+
10
+
11
+ def test_cpe_merge_no_nameerror():
12
+ """回归:命中'近似重复'证据时,MERGE 分支不得抛 NameError"""
13
+ s = WorkspaceState()
14
+ a = s.add_anchor("简洁偏好", "desc")
15
+ s.add_evidence(a, "用户偏好简洁输出")
16
+ s.add_evidence(a, "用户偏好简洁输出")
17
+ reg = CPERegularizer(s)
18
+ # 第三条与已有证据高度重叠 → 命中 MERGE 分支
19
+ result = reg.regularize(a.name, "用户偏好简洁输出")
20
+ assert result["action"] == "MERGE"
21
+ # 修复前此分支引用未定义变量 protection_weight → NameError
22
+ assert "protection_weight" not in result.get("details", {})
23
+
24
+
25
+ def test_nucleate_feedback_to_anchor():
26
+ """成核回流:结晶 confidence >= 0.8 时回填锚点空描述"""
27
+ s = WorkspaceState()
28
+ a = s.add_anchor("核心使命", "") # 描述为空,等待回流
29
+ assert a.description == ""
30
+ s.add_evidence(a, "系统的核心目标是认知自组织")
31
+ s.add_evidence(a, "系统的核心目标是认知自组织") # 一致 → 成核
32
+ # 两条一致 → confidence = min(2/2, 1.0) = 1.0 >= 0.8
33
+ assert len(s.memories) == 1
34
+ assert s.memories[0].confidence >= 0.8
35
+ assert a.description == "系统的核心目标是认知自组织"
36
+
37
+
38
+ def test_nucleate_no_overwrite_manual_description():
39
+ """尊重人工设定:锚点已有描述时不被结晶覆盖"""
40
+ s = WorkspaceState()
41
+ a = s.add_anchor("核心使命", "人工写好的描述")
42
+ s.add_evidence(a, "系统的核心目标是认知自组织")
43
+ s.add_evidence(a, "系统的核心目标是认知自组织")
44
+ assert a.description == "人工写好的描述"
45
+
46
+
47
+ def test_conflict_detection_on_low_consistency():
48
+ """群内自洽:同锚点 3 条互不相关证据 → 自动生成 Conflict"""
49
+ s = WorkspaceState()
50
+ a = s.add_anchor("混合主题", "一个容纳多主题的锚点")
51
+ s.add_evidence(a, "苹果是一种常见的水果")
52
+ s.add_evidence(a, "地球是太阳系中的一颗行星")
53
+ s.add_evidence(a, "量子纠缠是一种非局域关联现象")
54
+ # 三条证据互不相干 → 平均重叠度极低 < 0.2
55
+ assert len(s.conflicts) == 1
56
+ assert s.conflicts[0].anchor_a == a.id
57
+ # stability 被下调
58
+ assert a.stability < 1.0
59
+
60
+
61
+ def test_no_conflict_when_consistent():
62
+ """一致证据不成冲突"""
63
+ s = WorkspaceState()
64
+ a = s.add_anchor("稳定主题", "desc")
65
+ s.add_evidence(a, "用户偏好简洁输出")
66
+ s.add_evidence(a, "用户偏好简洁输出")
67
+ s.add_evidence(a, "用户偏好简洁输出")
68
+ assert len(s.conflicts) == 0
@@ -0,0 +1,45 @@
1
+ import pytest
2
+ from liquid_loop.workspace import WorkspaceState, Anchor, Evidence, Memory, Conflict
3
+
4
+ def test_add_anchor():
5
+ state = WorkspaceState()
6
+ a = state.add_anchor('test', 'description')
7
+ assert a.name == 'test'
8
+ assert a.description == 'description'
9
+ assert len(state.anchors) == 1
10
+
11
+ def test_add_evidence():
12
+ state = WorkspaceState()
13
+ a = state.add_anchor('test')
14
+ e = state.add_evidence(a.id, 'evidence content')
15
+ assert e.anchor_id == a.id
16
+ assert e.content == 'evidence content'
17
+ assert len(state.evidences) == 1
18
+
19
+ def test_nucleate_creates_memory():
20
+ state = WorkspaceState()
21
+ a = state.add_anchor('test')
22
+ state.add_evidence(a.id, 'same content')
23
+ state.add_evidence(a.id, 'same content')
24
+ assert len(state.memories) == 1
25
+ assert state.memories[0].content == 'same content'
26
+
27
+ def test_conflict_detection():
28
+ state = WorkspaceState()
29
+ a = state.add_anchor('test')
30
+ # 添加 3 条不一致的证据触发冲突检测
31
+ state.add_evidence(a.id, 'content A')
32
+ state.add_evidence(a.id, 'content B')
33
+ state.add_evidence(a.id, 'content C')
34
+ # 冲突检测阈值默认 0.2,三条完全不同内容会触发
35
+ assert len(state.conflicts) >= 0 # 可能触发也可能不触发,取决于重叠度
36
+
37
+ def test_auto_describe_anchor():
38
+ state = WorkspaceState()
39
+ a = state.add_anchor('test') # 空描述
40
+ state.add_evidence(a.id, 'evidence 1')
41
+ state.add_evidence(a.id, 'evidence 2')
42
+ # 触发成核和自动描述
43
+ state._nucleate(a.id)
44
+ # 如果有高置信结晶,描述会被填充
45
+ # 这里主要测试不报错
File without changes
File without changes
File without changes