liquid-loop 1.8.2__tar.gz → 1.8.3__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.
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/PKG-INFO +1 -1
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/liquid_loop/__init__.py +5 -2
- liquid_loop-1.8.3/liquid_loop/context_compress.py +155 -0
- liquid_loop-1.8.3/liquid_loop/recall_filter.py +126 -0
- liquid_loop-1.8.3/liquid_loop/self_eval.py +164 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/liquid_loop/workspace.py +11 -2
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/liquid_loop.egg-info/PKG-INFO +1 -1
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/liquid_loop.egg-info/SOURCES.txt +6 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/pyproject.toml +1 -1
- liquid_loop-1.8.3/tests/test_context_compress.py +102 -0
- liquid_loop-1.8.3/tests/test_recall_filter.py +79 -0
- liquid_loop-1.8.3/tests/test_self_eval.py +77 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/tests/test_semantica_borrow.py +23 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/LICENSE +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/README.md +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/liquid_loop/__main__.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/liquid_loop/cli.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/liquid_loop/cognitive_budget.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/liquid_loop/entropy.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/liquid_loop/liquid_reweight.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/liquid_loop/rar.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/liquid_loop/selfspin.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/liquid_loop/storage.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/liquid_loop.egg-info/dependency_links.txt +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/liquid_loop.egg-info/entry_points.txt +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/liquid_loop.egg-info/requires.txt +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/liquid_loop.egg-info/top_level.txt +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/liquid_loop_exp/liquid_persist_ab.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/liquid_loop_exp/liquid_recall_ab.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/setup.cfg +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/tests/test_attention_gain.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/tests/test_auth_guard.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/tests/test_cli_version.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/tests/test_consensus_expansion.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/tests/test_entropy.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/tests/test_layer1_persistence.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/tests/test_lifecycle.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/tests/test_liquid_reweight.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/tests/test_nucleate_dual_track.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/tests/test_peek_seal.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/tests/test_rar.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/tests/test_replay_pressure.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/tests/test_seal_persistence.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/tests/test_self_evolve.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/tests/test_selfspin.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.3}/tests/test_workspace.py +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"""Liquid Loop — Workspace Cognitive Runtime v1.8.
|
|
2
|
-
__version__ = "1.8.
|
|
1
|
+
"""Liquid Loop — Workspace Cognitive Runtime v1.8.3 (禁向量·活态液态神经网络:成核门槛/反证轨/老化回收/原理优先成核/因果演化/软取代supersede+治理查询state_at/impact/duplicates)"""
|
|
2
|
+
__version__ = "1.8.3"
|
|
3
3
|
|
|
4
4
|
from .workspace import (
|
|
5
5
|
WorkspaceState, Anchor, Evidence, Memory, Conflict,
|
|
@@ -9,3 +9,6 @@ from .storage import load, save, locked_state
|
|
|
9
9
|
from .entropy import calculate, calculate_detail, calculate as calculate_entropy
|
|
10
10
|
from .selfspin import LiquidSelfSpin
|
|
11
11
|
from .liquid_reweight import LiquidReweight
|
|
12
|
+
from .context_compress import ExtractiveCondenser, compress_context, CondenseReport
|
|
13
|
+
from .self_eval import consensus_self_check, diverg_beta, majority_vote, u_opsd_step
|
|
14
|
+
from .recall_filter import recall_content_filter, content_aware_filter, cosine_dict
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""长会话/长上下文提取式压缩(蒸馏自 Octomind condense.rs,落地液环)
|
|
2
|
+
|
|
3
|
+
把外部 runtime 的「提取式上下文压缩」蒸馏为液环一等公民组件:
|
|
4
|
+
- 超阈值触发 → 选行保留(非生成式摘要)
|
|
5
|
+
- 单条 < MIN_CANDIDATE_TOKENS 绝不触碰
|
|
6
|
+
- 压后 ≥ 压前则保留原文(no-gain 保护)
|
|
7
|
+
- 任何异常保留原文(fail-open)
|
|
8
|
+
|
|
9
|
+
契合液环哲学:零向量、确定性、零丢失。
|
|
10
|
+
压缩仅裁剪噪声行,原文语义结构(决策/错误/文件路径/代码/结论)保留;
|
|
11
|
+
底层 storage 完整存档,「蒸馏」与「归档」互不干扰。
|
|
12
|
+
|
|
13
|
+
触发阈值:环境变量 LIQUID_CONTEXT_COMPRESS_TOKENS(默认 0 = 不压缩),
|
|
14
|
+
与 LIQUID_EVIDENCE_BUDGET 运维风格一致。
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def estimate_tokens(text: str) -> int:
|
|
25
|
+
"""字符数 / 4 启发式(对预算触发判断足够精确,零依赖 tiktoken)。"""
|
|
26
|
+
if not text:
|
|
27
|
+
return 0
|
|
28
|
+
return max(1, len(text) // 4)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# 高价值行信号:保留含错误/决策/文件路径/代码定义/符号的行
|
|
32
|
+
_HIGH_VALUE = re.compile(
|
|
33
|
+
r"(?i)("
|
|
34
|
+
r"\b(error|failed|exception|traceback|panic)\b" # 错误/异常
|
|
35
|
+
r"|\b(decision|changed|fixed|result|todo|fix|note)\b" # 决策/结论
|
|
36
|
+
r"|[\w./\-]+\.(py|rs|ts|js|go|toml|json|md|yaml|yml)" # 文件路径
|
|
37
|
+
r"|^\s*(def|fn|func|class|impl|pub|import|export)\b" # 代码定义
|
|
38
|
+
r"|=>|==|!=|::|->" # 代码符号
|
|
39
|
+
r")"
|
|
40
|
+
)
|
|
41
|
+
# 噪声行信号:空/纯符号行、闲聊开场
|
|
42
|
+
_NOISE = re.compile(
|
|
43
|
+
r"(?i)^[\s>*\-–·•]*$"
|
|
44
|
+
r"|^(thinking|let me|i'll|sure|okay|great|here's)\b"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class CondenseReport:
|
|
50
|
+
"""压缩运行报告(对齐原版统计字段)。"""
|
|
51
|
+
|
|
52
|
+
triggered: bool = False
|
|
53
|
+
candidates: int = 0
|
|
54
|
+
condensed: int = 0
|
|
55
|
+
untouched: int = 0
|
|
56
|
+
saved_tokens: int = 0
|
|
57
|
+
notes: list = field(default_factory=list)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ExtractiveCondenser:
|
|
61
|
+
"""超阈值触发 → 选行保留(非生成式摘要)→ fail-open 不崩。
|
|
62
|
+
|
|
63
|
+
与原版 condense_round 不变量对齐:
|
|
64
|
+
- 整轮 token 总和 > threshold 才触发(防小结果免费注入大轮)
|
|
65
|
+
- 单条 < MIN_CANDIDATE_TOKENS 绝不触碰
|
|
66
|
+
- 压后 ≥ 压前则保留原文 (no-gain)
|
|
67
|
+
- 任何异常返回原样 (fail-open)
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
MIN_CANDIDATE_TOKENS = 512
|
|
71
|
+
KEEP_RATIO = 0.5 # 候选结果最多保留的行比例上限
|
|
72
|
+
|
|
73
|
+
def __init__(self, tokens_threshold: int = 0):
|
|
74
|
+
# 0 = 不压缩(缺省关闭,与 LIQUID_EVIDENCE_BUDGET 风格一致)
|
|
75
|
+
self.tokens_threshold = tokens_threshold
|
|
76
|
+
|
|
77
|
+
def _score_line(self, line: str) -> float:
|
|
78
|
+
"""启发式重要性打分(替代原版 cheap-LLM 行选择,零 LLM 依赖)。"""
|
|
79
|
+
s = 0.0
|
|
80
|
+
if _HIGH_VALUE.search(line):
|
|
81
|
+
s += 3.0
|
|
82
|
+
if _NOISE.search(line):
|
|
83
|
+
s -= 2.0
|
|
84
|
+
stripped = line.strip()
|
|
85
|
+
if len(stripped) >= 20:
|
|
86
|
+
s += 1.0
|
|
87
|
+
# 近似重复行惩罚
|
|
88
|
+
if len(stripped) > 40 and stripped.count(stripped[:10]) > 2:
|
|
89
|
+
s -= 1.5
|
|
90
|
+
return s
|
|
91
|
+
|
|
92
|
+
def condense_round(self, results: list) -> tuple:
|
|
93
|
+
"""results: list of dict {"tool": str, "content": str}
|
|
94
|
+
|
|
95
|
+
返回 (压缩后 results, CondenseReport)。fail-open 时 triggered=False。
|
|
96
|
+
"""
|
|
97
|
+
rep = CondenseReport()
|
|
98
|
+
try:
|
|
99
|
+
sizes = [estimate_tokens(r.get("content", "")) for r in results]
|
|
100
|
+
if sum(sizes) <= self.tokens_threshold:
|
|
101
|
+
return results, rep # 未触发
|
|
102
|
+
rep.triggered = True
|
|
103
|
+
|
|
104
|
+
candidates = [i for i, t in enumerate(sizes)
|
|
105
|
+
if t >= self.MIN_CANDIDATE_TOKENS]
|
|
106
|
+
rep.candidates = len(candidates)
|
|
107
|
+
if not candidates:
|
|
108
|
+
return results, rep
|
|
109
|
+
|
|
110
|
+
out = list(results)
|
|
111
|
+
for idx in candidates:
|
|
112
|
+
r = results[idx]
|
|
113
|
+
original = r.get("content", "")
|
|
114
|
+
before = estimate_tokens(original)
|
|
115
|
+
lines = original.splitlines()
|
|
116
|
+
if len(lines) <= 2:
|
|
117
|
+
rep.untouched += 1
|
|
118
|
+
continue
|
|
119
|
+
scored = [(self._score_line(ln), i, ln) for i, ln in enumerate(lines)]
|
|
120
|
+
scored.sort(key=lambda x: x[0], reverse=True)
|
|
121
|
+
keep_n = max(2, int(len(lines) * self.KEEP_RATIO))
|
|
122
|
+
keep_idx = sorted(i for _, i, _ in scored[:keep_n])
|
|
123
|
+
new_content = "\n".join(lines[i] for i in keep_idx)
|
|
124
|
+
after = estimate_tokens(new_content)
|
|
125
|
+
if after >= before: # no-gain 保护
|
|
126
|
+
rep.untouched += 1
|
|
127
|
+
continue
|
|
128
|
+
out[idx] = {"tool": r.get("tool", ""), "content": new_content}
|
|
129
|
+
rep.condensed += 1
|
|
130
|
+
rep.saved_tokens += (before - after)
|
|
131
|
+
if rep.condensed:
|
|
132
|
+
rep.notes.append("📎 CONDENSED by supervisor")
|
|
133
|
+
return out, rep
|
|
134
|
+
except Exception as e:
|
|
135
|
+
rep.notes.append(f"fail-open: {e}")
|
|
136
|
+
return results, rep
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def compress_context(
|
|
140
|
+
texts: list,
|
|
141
|
+
threshold: Optional[int] = None,
|
|
142
|
+
) -> tuple:
|
|
143
|
+
"""液环便捷入口:把多条证据/上下文作为一轮压缩。
|
|
144
|
+
|
|
145
|
+
texts: 待压缩文本列表(如 recall 返回的多条证据 content)
|
|
146
|
+
threshold: 触发阈值 token;None 时读 LIQUID_CONTEXT_COMPRESS_TOKENS(默认 0=不压)
|
|
147
|
+
返回 (压缩后文本列表, CondenseReport)
|
|
148
|
+
"""
|
|
149
|
+
if threshold is None:
|
|
150
|
+
threshold = int(os.environ.get("LIQUID_CONTEXT_COMPRESS_TOKENS", "0"))
|
|
151
|
+
if threshold <= 0:
|
|
152
|
+
return list(texts), CondenseReport() # 未启用
|
|
153
|
+
results = [{"tool": "", "content": t} for t in texts]
|
|
154
|
+
out, rep = ExtractiveCondenser(threshold).condense_round(results)
|
|
155
|
+
return [r["content"] for r in out], rep
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""液环召回相关性过滤(蒸馏自 RobSelf arXiv:2602.18822,落地 recall 过滤)
|
|
2
|
+
|
|
3
|
+
RobSelf 两大组件:
|
|
4
|
+
① 错位感知特征转换器:局部窗口搜索吸收空间错位(无需预对齐)
|
|
5
|
+
② 内容感知参考滤波器:按内容相似度保留/抑制参考(去噪)
|
|
6
|
+
蒸馏到液环 recall:在 jaccard 主相关性之外,加一层「内容感知 + 错位容忍」的
|
|
7
|
+
辅助相关性打分,并可选抑制近零权重噪声候选。
|
|
8
|
+
|
|
9
|
+
- content_aware_weight:token 词频向量的余弦相似度(分布级相关性)。
|
|
10
|
+
比 jaccard 更能识别「只共享个别稀有 token 的噪声候选」→ RobSelf 去噪。
|
|
11
|
+
- aligned_score:错位容忍对齐分 = max(jaccard, 含容度(q⊆c), 含容度(c⊆q))。
|
|
12
|
+
吸收查询与记忆间的措辞/语序错位(paraphrase / 子集包含),即 RobSelf ①。
|
|
13
|
+
|
|
14
|
+
零爆破半径设计:
|
|
15
|
+
- 默认仅增强(给每条候选附加两个 metadata 字段),绝不重排 recall 顺序。
|
|
16
|
+
- 仅当调用方显式给 θ∈(0,1) 时才抑制 content_aware_weight<θ 的噪声候选。
|
|
17
|
+
- fail-open:异常由 caller 兜底(返回原样)。
|
|
18
|
+
|
|
19
|
+
零依赖:仅 Python 标准库(math, re, collections)。
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import math
|
|
24
|
+
import re
|
|
25
|
+
from collections import Counter
|
|
26
|
+
|
|
27
|
+
# 与 server 同源的 token 切分
|
|
28
|
+
_TOKEN = re.compile(r"[一-鿿]|[a-zA-Z0-9]+")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _tokens(s: str) -> list:
|
|
32
|
+
return _TOKEN.findall(s or "")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _vec(s: str) -> dict:
|
|
36
|
+
"""字符串 → 词频分布向量 {token: freq}。空串返回 {}。"""
|
|
37
|
+
toks = _tokens(s)
|
|
38
|
+
if not toks:
|
|
39
|
+
return {}
|
|
40
|
+
n = len(toks)
|
|
41
|
+
c = Counter(toks)
|
|
42
|
+
return {t: v / n for t, v in c.items()}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def cosine_dict(a: dict, b: dict) -> float:
|
|
46
|
+
"""两分布向量的余弦相似度(内容感知打分核心)。"""
|
|
47
|
+
keys = set(a) | set(b)
|
|
48
|
+
if not keys:
|
|
49
|
+
return 0.0
|
|
50
|
+
dot = sum(a.get(k, 0.0) * b.get(k, 0.0) for k in keys)
|
|
51
|
+
na = math.sqrt(sum(v * v for v in a.values()))
|
|
52
|
+
nb = math.sqrt(sum(v * v for v in b.values()))
|
|
53
|
+
if na == 0.0 or nb == 0.0:
|
|
54
|
+
return 0.0
|
|
55
|
+
return dot / (na * nb)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _jaccard_sets(a: set, b: set) -> float:
|
|
59
|
+
if not a or not b:
|
|
60
|
+
return 0.0
|
|
61
|
+
return len(a & b) / len(a | b)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _containment_sets(a: set, b: set) -> float:
|
|
65
|
+
"""重叠系数 |A∩B| / min(|A|,|B|):对子集包含(错位/部分匹配)鲁棒。"""
|
|
66
|
+
if not a or not b:
|
|
67
|
+
return 0.0
|
|
68
|
+
return len(a & b) / min(len(a), len(b))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def content_aware_filter(ref_vec: dict, tgt_vec: dict, theta: float):
|
|
72
|
+
"""内容感知参考滤波器(RobSelf ② 落地):
|
|
73
|
+
计算参考与目标的内容相似度;≥θ 保留权重1,<θ 软抑制(权重=sim/θ)。
|
|
74
|
+
返回 (weight, sim)。不变量:抑制噪声/不匹配区域,保留有用内容。"""
|
|
75
|
+
sim = cosine_dict(ref_vec, tgt_vec)
|
|
76
|
+
weight = 1.0 if sim >= theta else max(sim / theta, 0.0) if theta > 0 else sim
|
|
77
|
+
return weight, sim
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def recall_content_filter(query: str, candidates: list, theta: float = 0.0,
|
|
81
|
+
window: int = 3) -> tuple:
|
|
82
|
+
"""对 recall 候选做内容感知 + 错位容忍增强(RobSelf 蒸馏落地)。
|
|
83
|
+
|
|
84
|
+
参数:
|
|
85
|
+
query — 查询串
|
|
86
|
+
candidates— recall 返回的候选 dict 列表(需含 "content")
|
|
87
|
+
theta — 噪声抑制阈值(0=仅增强不抑制;0<θ<1=抑制 weight<θ 的候选)
|
|
88
|
+
window — 错位容忍窗口(保留接口,当前对齐用含容度吸收错位)
|
|
89
|
+
返回 (过滤后候选列表, report)。
|
|
90
|
+
· 每条保留候选附加 content_aware_weight / aligned_score 两字段
|
|
91
|
+
· theta>0 时丢弃 content_aware_weight<θ 的噪声候选(RobSelf 去噪)
|
|
92
|
+
不变量:绝不重排(保留原 jaccard score 作为主排序);fail-open 由 caller 包裹。
|
|
93
|
+
"""
|
|
94
|
+
q_set = set(_tokens(query))
|
|
95
|
+
q_vec = _vec(query)
|
|
96
|
+
kept = []
|
|
97
|
+
suppressed = 0
|
|
98
|
+
for r in candidates:
|
|
99
|
+
content = r.get("content", "") or ""
|
|
100
|
+
c_set = set(_tokens(content))
|
|
101
|
+
c_vec = _vec(content)
|
|
102
|
+
|
|
103
|
+
# ① 内容感知权重(RobSelf ②)
|
|
104
|
+
if theta > 0:
|
|
105
|
+
weight, _ = content_aware_filter(c_vec, q_vec, theta)
|
|
106
|
+
else:
|
|
107
|
+
weight = cosine_dict(c_vec, q_vec) # 纯相似度,仅展示
|
|
108
|
+
|
|
109
|
+
# ② 错位容忍对齐分(RobSelf ①):吸收措辞/语序/子集包含错位
|
|
110
|
+
jac = _jaccard_sets(q_set, c_set)
|
|
111
|
+
cont_qc = _containment_sets(q_set, c_set) # query 是候选的子集
|
|
112
|
+
cont_cq = _containment_sets(c_set, q_set) # 候选是 query 的子集
|
|
113
|
+
aligned = max(jac, cont_qc, cont_cq)
|
|
114
|
+
|
|
115
|
+
if theta > 0 and weight < theta:
|
|
116
|
+
suppressed += 1
|
|
117
|
+
continue # 噪声候选:内容感知低相似 → 抑制(不进入召回)
|
|
118
|
+
|
|
119
|
+
new_r = dict(r) # 复制,避免污染原 dict
|
|
120
|
+
new_r["content_aware_weight"] = round(weight, 4)
|
|
121
|
+
new_r["aligned_score"] = round(aligned, 4)
|
|
122
|
+
kept.append(new_r)
|
|
123
|
+
|
|
124
|
+
rep = {"enabled": True, "theta": theta, "n_in": len(candidates),
|
|
125
|
+
"n_out": len(kept), "suppressed": suppressed}
|
|
126
|
+
return kept, rep
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""液环自评估(蒸馏自 U-OPSD arXiv:2608.06296,落地液环自检环节)
|
|
2
|
+
|
|
3
|
+
U-OPSD 核心:无外部真值自评估——采样 G 条 rollout → 多数投票 → 最长同意 rollout
|
|
4
|
+
为伪解 → 沿不同意 rollout 前缀逐 token D_β 蒸馏。蒸馏到液环的「自检环节」:
|
|
5
|
+
|
|
6
|
+
- consensus_self_check(answers, tau):给定一组候选答案(如 Vera 对同一提议的
|
|
7
|
+
多条推理路径 / 改写),算共识票比 frac + 是否达 τ 触发 + 伪解;无需外部真值,
|
|
8
|
+
即给 AI 提议做一致性自检打分。vera-self-evolve 可经 8790 /selfcheck 调用。
|
|
9
|
+
- diverg_beta(p, q, beta):β-散度(β=0.5 JSD 对称 / β=1 reverse KL 非对称)。
|
|
10
|
+
- u_opsd_step:原版三步骤(token 级 rollout 分布版),留给有真实策略分布的 caller。
|
|
11
|
+
|
|
12
|
+
与 vera-self-evolve v2「RSI 内化」强同构:U-OPSD 的「无外部真值自评估」正是
|
|
13
|
+
自进化评估器的确定性落地——不依赖外部标注即可给提议打一致性分(紧致度=可信度)。
|
|
14
|
+
零依赖:仅 Python 标准库(math, re, collections)。fail-open:异常由 caller 兜底。
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import math
|
|
19
|
+
import re
|
|
20
|
+
from collections import Counter
|
|
21
|
+
|
|
22
|
+
# 与 server 同源的 token 切分:中文单字 + 英文数字连续串
|
|
23
|
+
_TOKEN = re.compile(r"[一-鿿]|[a-zA-Z0-9]+")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _tokens(s: str) -> list:
|
|
27
|
+
return _TOKEN.findall(s or "")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def majority_vote(answers):
|
|
31
|
+
"""多数投票:返回 (伪答案, 胜出票比例)。
|
|
32
|
+
|
|
33
|
+
对齐 U-OPSD:伪解来自多数票;frac 即共识强度(无需外部真值)。
|
|
34
|
+
"""
|
|
35
|
+
c = Counter(answers)
|
|
36
|
+
top, cnt = c.most_common(1)[0]
|
|
37
|
+
return top, cnt / len(answers)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def diverg_beta(p, q, beta):
|
|
41
|
+
"""β-散度 D_β(p∥q):
|
|
42
|
+
β=1 → reverse KL: Σ q·log(q/p)
|
|
43
|
+
β≈0 → forward KL: Σ p·log(p/q)(β→0 极限)
|
|
44
|
+
β=0.5 → JSD(对称):0.5·KL(P∥M)+0.5·KL(Q∥M), M=(P+Q)/2
|
|
45
|
+
论文 Table 5:β=0.5 标为 JSD,β=1 标为 reverse KL。
|
|
46
|
+
零依赖:标准库 math;p/q 为 {token: prob} 分布(缺失键按 1e-9 平滑)。
|
|
47
|
+
"""
|
|
48
|
+
tokens = set(p) | set(q)
|
|
49
|
+
P = {t: p.get(t, 1e-9) for t in tokens}
|
|
50
|
+
Q = {t: q.get(t, 1e-9) for t in tokens}
|
|
51
|
+
|
|
52
|
+
def kl(a, b):
|
|
53
|
+
return sum(a[t] * math.log(a[t] / b[t]) for t in tokens)
|
|
54
|
+
|
|
55
|
+
if beta >= 1.0:
|
|
56
|
+
return kl(Q, P) # reverse KL
|
|
57
|
+
if beta <= 0.0:
|
|
58
|
+
return kl(P, Q) # forward KL
|
|
59
|
+
# β=0.5 → JSD
|
|
60
|
+
M = {t: 0.5 * (P[t] + Q[t]) for t in tokens}
|
|
61
|
+
return 0.5 * kl(P, M) + 0.5 * kl(Q, M)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _dist_of(s: str) -> dict:
|
|
65
|
+
"""字符串 → 词频分布(token 级)。空串返回 {}。"""
|
|
66
|
+
toks = _tokens(s)
|
|
67
|
+
if not toks:
|
|
68
|
+
return {}
|
|
69
|
+
n = len(toks)
|
|
70
|
+
c = Counter(toks)
|
|
71
|
+
return {t: v / n for t, v in c.items()}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def string_divergence(a: str, b: str, beta: float = 0.5) -> float:
|
|
75
|
+
"""两条答案字符串间的 token 分布散度(U-OPSD D_β 的字符串级落地)。"""
|
|
76
|
+
return diverg_beta(_dist_of(a), _dist_of(b), beta)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def consensus_self_check(answers, tau: float = 0.5, beta: float = 0.5) -> dict:
|
|
80
|
+
"""字符串级共识自检(U-OPSD 落地版)—— 无外部真值,给提议打一致性分。
|
|
81
|
+
|
|
82
|
+
参数:
|
|
83
|
+
answers — 同一提议的多条候选答案(如多条推理路径 / 改写 / 重采样结果)
|
|
84
|
+
tau — 触发阈值(默认 0.5,对齐论文 τ=1/2)
|
|
85
|
+
beta — 散度阶(默认 0.5 JSD)
|
|
86
|
+
返回 dict:
|
|
87
|
+
triggered — 多数票比 ≥ τ 且存在分歧(与 U-OPSD 触发条件同构)
|
|
88
|
+
frac — 共识票比(共识强度)
|
|
89
|
+
pseudo_index — 伪解(最长同意答案)在原列表的索引
|
|
90
|
+
pseudo_answer — 伪解内容
|
|
91
|
+
mean_divergence — 同意集内平均两两散度(越小越紧致=越可信伪解)
|
|
92
|
+
n — 候选数
|
|
93
|
+
不变量:
|
|
94
|
+
① 触发条件:胜出票比 ≥ τ(=1/2) 且 至少一条答案不同意(𝒴⁻ ≠ ∅)
|
|
95
|
+
② 伪解 = 最长「同意」答案(argmax_{y∈𝒴⁺}|y|)
|
|
96
|
+
"""
|
|
97
|
+
if not answers:
|
|
98
|
+
return {"triggered": False, "frac": 0.0, "pseudo_index": -1,
|
|
99
|
+
"pseudo_answer": None, "mean_divergence": 0.0, "n": 0}
|
|
100
|
+
pseudo, frac = majority_vote(answers)
|
|
101
|
+
Y_plus_idx = [i for i, a in enumerate(answers) if a == pseudo]
|
|
102
|
+
Y_minus = [a for a in answers if a != pseudo]
|
|
103
|
+
|
|
104
|
+
triggered = (frac >= tau) and (len(Y_minus) > 0)
|
|
105
|
+
|
|
106
|
+
# 伪解 = 最长同意答案
|
|
107
|
+
pseudo_index = max(Y_plus_idx, key=lambda i: len(answers[i]))
|
|
108
|
+
|
|
109
|
+
# 同意集内平均两两散度(紧致度):越低→伪解越被多条路径共同支撑
|
|
110
|
+
plus = [answers[i] for i in Y_plus_idx]
|
|
111
|
+
if len(plus) >= 2:
|
|
112
|
+
divs = []
|
|
113
|
+
for i in range(len(plus)):
|
|
114
|
+
for j in range(i + 1, len(plus)):
|
|
115
|
+
divs.append(string_divergence(plus[i], plus[j], beta))
|
|
116
|
+
mean_div = sum(divs) / len(divs) if divs else 0.0
|
|
117
|
+
else:
|
|
118
|
+
mean_div = 0.0
|
|
119
|
+
|
|
120
|
+
return {"triggered": triggered, "frac": frac, "pseudo_index": pseudo_index,
|
|
121
|
+
"pseudo_answer": pseudo, "mean_divergence": mean_div, "n": len(answers)}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------- 策略分布版(进阶,留给有真实 π/π̄ 的 caller)----------
|
|
125
|
+
def u_opsd_step(rollouts, pseudo_pick, tau: float = 0.5, beta: float = 0.5,
|
|
126
|
+
teacher_dist=None, student_dist=None) -> tuple:
|
|
127
|
+
"""token 级 U-OPSD 核心三步(蒸馏版,分布由 caller 提供)。
|
|
128
|
+
|
|
129
|
+
参数:
|
|
130
|
+
rollouts — 每条 rollout 为 (answer, tokens_tuple)
|
|
131
|
+
pseudo_pick — 选伪解函数:给定同意 rollouts 返回伪解 tokens(默认最长)
|
|
132
|
+
teacher_dist — fn(ctx_tokens) → {token: prob}(stop-grad 副本 π̄)
|
|
133
|
+
student_dist — fn(ctx_tokens) → {token: prob}(可训练 π_θ)
|
|
134
|
+
返回 (loss, info)。loss 仅在触发时为正(未触发=0.0)。
|
|
135
|
+
不变量对齐论文 ℒ_U-OPSD:触发→选伪解→仅对 𝒴⁻ 前缀逐 token D_β。
|
|
136
|
+
"""
|
|
137
|
+
if teacher_dist is None or student_dist is None:
|
|
138
|
+
raise ValueError("u_opsd_step 需要 teacher_dist / student_dist 策略分布函数")
|
|
139
|
+
answers = [r[0] for r in rollouts]
|
|
140
|
+
pseudo, frac = majority_vote(answers)
|
|
141
|
+
Y_plus = [r for r, a in zip(rollouts, answers) if a == pseudo]
|
|
142
|
+
Y_minus = [r for r, a in zip(rollouts, answers) if a != pseudo]
|
|
143
|
+
|
|
144
|
+
triggered = (frac >= tau) and (len(Y_minus) > 0)
|
|
145
|
+
if not triggered:
|
|
146
|
+
return 0.0, {"triggered": False, "frac": frac, "n_minus": len(Y_minus),
|
|
147
|
+
"pseudo": pseudo, "loss": 0.0}
|
|
148
|
+
|
|
149
|
+
y_plus = pseudo_pick(Y_plus) if pseudo_pick else max(Y_plus, key=lambda r: len(r[1]))[1]
|
|
150
|
+
|
|
151
|
+
total = 0.0
|
|
152
|
+
n_terms = 0
|
|
153
|
+
for (_, y_minus_toks) in Y_minus:
|
|
154
|
+
for n in range(1, len(y_minus_toks)):
|
|
155
|
+
teacher_ctx = list(y_plus) + list(y_minus_toks[:n])
|
|
156
|
+
student_ctx = list(y_minus_toks[:n])
|
|
157
|
+
p_t = teacher_dist(teacher_ctx) # π̄(·|x, y⁺, y⁻<n)
|
|
158
|
+
p_s = student_dist(student_ctx) # π_θ(·|x, y⁻<n)
|
|
159
|
+
total += diverg_beta(p_t, p_s, beta)
|
|
160
|
+
n_terms += 1
|
|
161
|
+
|
|
162
|
+
loss = total / n_terms if n_terms else 0.0
|
|
163
|
+
return loss, {"triggered": True, "frac": frac, "pseudo": pseudo,
|
|
164
|
+
"n_minus": len(Y_minus), "n_terms": n_terms, "loss": loss}
|
|
@@ -284,8 +284,8 @@ def _get_version() -> str:
|
|
|
284
284
|
pyproject = Path(__file__).parent.parent / "pyproject.toml"
|
|
285
285
|
if pyproject.exists():
|
|
286
286
|
data = tomllib.loads(pyproject.read_text())
|
|
287
|
-
return data.get("project", {}).get("version", "1.8.
|
|
288
|
-
return "1.8.
|
|
287
|
+
return data.get("project", {}).get("version", "1.8.3")
|
|
288
|
+
return "1.8.3"
|
|
289
289
|
|
|
290
290
|
@dataclass
|
|
291
291
|
class WorkspaceState:
|
|
@@ -770,6 +770,8 @@ class WorkspaceState:
|
|
|
770
770
|
- loser==winner → 拒绝(自取代无意义)
|
|
771
771
|
- winner 不存在 → 拒绝(取代者须先存在,否则丧失血缘根基)
|
|
772
772
|
- 重复取代同一 winner → 幂等(supersedes 去重,不堆叠)
|
|
773
|
+
- v1.8.3 consensus 保护(Palantir owned 细化):loser 若是 consensus 结晶的证据,
|
|
774
|
+
禁止单边软取代——共识证据属于全体贡献者,只能走全员 dissolve(与 delete_as 同语义)。
|
|
773
775
|
"""
|
|
774
776
|
if loser_id == winner_id:
|
|
775
777
|
return {"ok": False, "error": "self_supersede_rejected", "loser_id": loser_id}
|
|
@@ -778,6 +780,13 @@ class WorkspaceState:
|
|
|
778
780
|
return {"ok": False, "error": "loser_not_found", "loser_id": loser_id}
|
|
779
781
|
if winner_id not in by_id:
|
|
780
782
|
return {"ok": False, "error": "winner_not_found", "winner_id": winner_id}
|
|
783
|
+
# v1.8.3 consensus 保护:consensus 结晶的证据禁止单边软取代(防 admin 误伤共识)
|
|
784
|
+
consensus_evidence = {eid for m in self.memories if m.scope == "consensus"
|
|
785
|
+
for eid in m.evidence_ids}
|
|
786
|
+
if loser_id in consensus_evidence:
|
|
787
|
+
return {"ok": False,
|
|
788
|
+
"error": "forbidden: consensus evidence requires unanimous dissolution, not single supersede",
|
|
789
|
+
"loser_id": loser_id}
|
|
781
790
|
loser = by_id[loser_id]
|
|
782
791
|
winner = by_id[winner_id]
|
|
783
792
|
ts = now()
|
|
@@ -5,9 +5,12 @@ liquid_loop/__init__.py
|
|
|
5
5
|
liquid_loop/__main__.py
|
|
6
6
|
liquid_loop/cli.py
|
|
7
7
|
liquid_loop/cognitive_budget.py
|
|
8
|
+
liquid_loop/context_compress.py
|
|
8
9
|
liquid_loop/entropy.py
|
|
9
10
|
liquid_loop/liquid_reweight.py
|
|
10
11
|
liquid_loop/rar.py
|
|
12
|
+
liquid_loop/recall_filter.py
|
|
13
|
+
liquid_loop/self_eval.py
|
|
11
14
|
liquid_loop/selfspin.py
|
|
12
15
|
liquid_loop/storage.py
|
|
13
16
|
liquid_loop/workspace.py
|
|
@@ -23,6 +26,7 @@ tests/test_attention_gain.py
|
|
|
23
26
|
tests/test_auth_guard.py
|
|
24
27
|
tests/test_cli_version.py
|
|
25
28
|
tests/test_consensus_expansion.py
|
|
29
|
+
tests/test_context_compress.py
|
|
26
30
|
tests/test_entropy.py
|
|
27
31
|
tests/test_layer1_persistence.py
|
|
28
32
|
tests/test_lifecycle.py
|
|
@@ -30,8 +34,10 @@ tests/test_liquid_reweight.py
|
|
|
30
34
|
tests/test_nucleate_dual_track.py
|
|
31
35
|
tests/test_peek_seal.py
|
|
32
36
|
tests/test_rar.py
|
|
37
|
+
tests/test_recall_filter.py
|
|
33
38
|
tests/test_replay_pressure.py
|
|
34
39
|
tests/test_seal_persistence.py
|
|
40
|
+
tests/test_self_eval.py
|
|
35
41
|
tests/test_self_evolve.py
|
|
36
42
|
tests/test_selfspin.py
|
|
37
43
|
tests/test_semantica_borrow.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "liquid-loop"
|
|
7
|
-
version = "1.8.
|
|
7
|
+
version = "1.8.3"
|
|
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,102 @@
|
|
|
1
|
+
"""液环长会话提取式压缩落地测试(蒸馏自 Octomind condense.rs)"""
|
|
2
|
+
import pytest
|
|
3
|
+
from liquid_loop.context_compress import (
|
|
4
|
+
ExtractiveCondenser,
|
|
5
|
+
compress_context,
|
|
6
|
+
estimate_tokens,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _big_text() -> str:
|
|
11
|
+
"""单条 ≥ 512 token(≈2048 字符)才入候选。"""
|
|
12
|
+
return (
|
|
13
|
+
"error: connection refused\n"
|
|
14
|
+
+ "okay sure let me think\n" * 5
|
|
15
|
+
+ "def fix(): return True\n/path/to/module.py:42 changed\n"
|
|
16
|
+
+ "result: 3 rows updated\n"
|
|
17
|
+
+ "blah blah noise line\n" * 100
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ───────────── 核心不变量 ─────────────
|
|
22
|
+
|
|
23
|
+
def test_no_trigger_below_threshold():
|
|
24
|
+
cond = ExtractiveCondenser(tokens_threshold=100000)
|
|
25
|
+
texts = ["short line one", "short line two"]
|
|
26
|
+
out, rep = cond.condense_round([{"tool": "", "content": t} for t in texts])
|
|
27
|
+
assert rep.triggered is False
|
|
28
|
+
assert rep.condensed == 0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_candidate_below_min_not_touched():
|
|
32
|
+
# 整轮超阈值,但单条 < 512 token → 不压缩任何条
|
|
33
|
+
small = "error: x\n" * 10 # 远小于 512 token
|
|
34
|
+
cond = ExtractiveCondenser(tokens_threshold=1)
|
|
35
|
+
out, rep = cond.condense_round([{"tool": "shell", "content": small}])
|
|
36
|
+
assert rep.triggered is True
|
|
37
|
+
assert rep.candidates == 0
|
|
38
|
+
assert rep.condensed == 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_condense_saves_tokens():
|
|
42
|
+
cond = ExtractiveCondenser(tokens_threshold=200)
|
|
43
|
+
out, rep = cond.condense_round([{"tool": "shell", "content": _big_text()}])
|
|
44
|
+
assert rep.triggered is True
|
|
45
|
+
assert rep.candidates == 1
|
|
46
|
+
assert rep.condensed == 1
|
|
47
|
+
assert rep.saved_tokens > 0
|
|
48
|
+
assert estimate_tokens(out[0]["content"]) < estimate_tokens(_big_text())
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_no_gain_keeps_original():
|
|
52
|
+
# 全是高价值短行(无可裁剪噪声)→ 不应越压越大,保留原文
|
|
53
|
+
cond = ExtractiveCondenser(tokens_threshold=1)
|
|
54
|
+
val = "def f(): return 1\n" * 5 # 每条短且高价值,压缩无收益
|
|
55
|
+
out, rep = cond.condense_round([{"tool": "x", "content": val}])
|
|
56
|
+
# 单条 < 512 token 不入候选,直接 untouched
|
|
57
|
+
assert rep.candidates == 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_fail_open_on_bad_input():
|
|
61
|
+
cond = ExtractiveCondenser(tokens_threshold=1)
|
|
62
|
+
# content 为非 str 真值(如 int)→ estimate_tokens 抛 TypeError,
|
|
63
|
+
# 被 try 捕获,fail-open 返回原样(不崩、不丢数据)
|
|
64
|
+
bad = [{"tool": "x", "content": 12345}]
|
|
65
|
+
out, rep = cond.condense_round(bad)
|
|
66
|
+
assert rep.notes and "fail-open" in rep.notes[0]
|
|
67
|
+
assert out == bad
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_none_content_safe_no_trigger():
|
|
71
|
+
# content=None 视为空,安全返回未触发(非异常路径)
|
|
72
|
+
cond = ExtractiveCondenser(tokens_threshold=1)
|
|
73
|
+
out, rep = cond.condense_round([{"tool": "x", "content": None}])
|
|
74
|
+
assert rep.triggered is False
|
|
75
|
+
assert rep.notes == []
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ───────────── 便捷入口 + env 阈值 ─────────────
|
|
79
|
+
|
|
80
|
+
def test_compress_context_env_threshold(monkeypatch):
|
|
81
|
+
monkeypatch.setenv("LIQUID_CONTEXT_COMPRESS_TOKENS", "200")
|
|
82
|
+
out, rep = compress_context([_big_text()])
|
|
83
|
+
assert rep.triggered is True
|
|
84
|
+
assert rep.condensed == 1
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_compress_context_disabled_by_default(monkeypatch):
|
|
88
|
+
monkeypatch.delenv("LIQUID_CONTEXT_COMPRESS_TOKENS", raising=False)
|
|
89
|
+
texts = [_big_text()]
|
|
90
|
+
out, rep = compress_context(texts)
|
|
91
|
+
assert rep.triggered is False # 默认 0 = 不压缩
|
|
92
|
+
assert out == texts
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_compress_context_multi_round():
|
|
96
|
+
# 多条文本作为一轮,总 token 超阈值才触发
|
|
97
|
+
cond = ExtractiveCondenser(tokens_threshold=300)
|
|
98
|
+
texts = [_big_text(), _big_text()]
|
|
99
|
+
out, rep = cond.condense_round([{"tool": "", "content": t} for t in texts])
|
|
100
|
+
assert rep.triggered is True
|
|
101
|
+
assert rep.candidates == 2
|
|
102
|
+
assert rep.condensed == 2
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""液环召回相关性过滤(蒸馏自 RobSelf)落地测试"""
|
|
2
|
+
import math
|
|
3
|
+
from liquid_loop.recall_filter import (
|
|
4
|
+
cosine_dict,
|
|
5
|
+
content_aware_filter,
|
|
6
|
+
recall_content_filter,
|
|
7
|
+
_vec,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_cosine_identical():
|
|
12
|
+
a = _vec("液环 禁 向量")
|
|
13
|
+
assert abs(cosine_dict(a, a) - 1.0) < 1e-9
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_cosine_orthogonal_zero():
|
|
17
|
+
a = _vec("苹果 香蕉")
|
|
18
|
+
b = _vec("西瓜 葡萄")
|
|
19
|
+
assert cosine_dict(a, b) == 0.0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_content_aware_filter_suppress_vs_keep():
|
|
23
|
+
hi = _vec("液环 禁 向量 记忆")
|
|
24
|
+
lo = _vec("苹果 香蕉 西瓜")
|
|
25
|
+
tgt = _vec("液环 禁 向量 自检")
|
|
26
|
+
w_hi, s_hi = content_aware_filter(hi, tgt, theta=0.3)
|
|
27
|
+
w_lo, s_lo = content_aware_filter(lo, tgt, theta=0.3)
|
|
28
|
+
assert w_hi == 1.0 and s_hi >= 0.3
|
|
29
|
+
assert w_lo < 1.0 and s_lo < 0.3 # 低相似被软抑制
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_augment_only_attaches_metadata():
|
|
33
|
+
# theta=0 → 仅增强,不抑制,所有候选保留且附字段
|
|
34
|
+
cands = [
|
|
35
|
+
{"memory_id": "e1", "type": "evidence", "content": "液环禁向量记忆系统"},
|
|
36
|
+
{"memory_id": "m1", "type": "memory", "content": "共识结晶内容"},
|
|
37
|
+
]
|
|
38
|
+
out, rep = recall_content_filter("液环 向量", cands, theta=0.0)
|
|
39
|
+
assert rep["suppressed"] == 0
|
|
40
|
+
assert len(out) == 2
|
|
41
|
+
assert "content_aware_weight" in out[0]
|
|
42
|
+
assert "aligned_score" in out[0]
|
|
43
|
+
# 记忆候选也附加 metadata(未抑制)
|
|
44
|
+
assert "content_aware_weight" in out[1]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_suppress_noise_at_theta():
|
|
48
|
+
# theta=0.3 → 内容无关候选被抑制
|
|
49
|
+
cands = [
|
|
50
|
+
{"memory_id": "e1", "type": "evidence", "content": "液环禁向量记忆系统"},
|
|
51
|
+
{"memory_id": "e2", "type": "evidence", "content": "苹果 香蕉 西瓜 葡萄"},
|
|
52
|
+
]
|
|
53
|
+
out, rep = recall_content_filter("液环 向量", cands, theta=0.3)
|
|
54
|
+
assert rep["suppressed"] == 1
|
|
55
|
+
assert len(out) == 1
|
|
56
|
+
assert out[0]["memory_id"] == "e1"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_aligned_score_absorbs_subset_misalignment():
|
|
60
|
+
# query 是候选的子集 → 错位容忍对齐分应 > 纯 jaccard(子集被识别为相关)
|
|
61
|
+
cands = [{"memory_id": "e1", "type": "evidence",
|
|
62
|
+
"content": "液环 禁 向量 记忆 系统 自检 环节"}]
|
|
63
|
+
out, _ = recall_content_filter("液环 向量", cands, theta=0.0)
|
|
64
|
+
# aligned_score = max(jaccard, containment(q⊆c), containment(c⊆q))
|
|
65
|
+
# query⊆candidate → containment(q⊆c) = 1.0
|
|
66
|
+
assert out[0]["aligned_score"] == 1.0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_recall_order_preserved_no_rerank():
|
|
70
|
+
# 过滤绝不重排:输入顺序保持
|
|
71
|
+
cands = [
|
|
72
|
+
{"memory_id": "e1", "type": "evidence", "content": "苹果 香蕉"},
|
|
73
|
+
{"memory_id": "e2", "type": "evidence", "content": "液环 向量 记忆"},
|
|
74
|
+
{"memory_id": "e3", "type": "evidence", "content": "西瓜 葡萄"},
|
|
75
|
+
]
|
|
76
|
+
# theta=0.3 → e1/e3 被抑制,仅 e2 保留,但顺序应紧随其原位置(输出顺序=输入顺序)
|
|
77
|
+
out, rep = recall_content_filter("液环 向量", cands, theta=0.3)
|
|
78
|
+
assert [r["memory_id"] for r in out] == ["e2"]
|
|
79
|
+
assert rep["n_in"] == 3
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""液环自检(蒸馏自 U-OPSD)落地测试"""
|
|
2
|
+
import math
|
|
3
|
+
from liquid_loop.self_eval import (
|
|
4
|
+
majority_vote,
|
|
5
|
+
diverg_beta,
|
|
6
|
+
consensus_self_check,
|
|
7
|
+
string_divergence,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_majority_vote():
|
|
12
|
+
ans = ["A", "A", "B", "A", "C"]
|
|
13
|
+
top, frac = majority_vote(ans)
|
|
14
|
+
assert top == "A"
|
|
15
|
+
assert abs(frac - 0.6) < 1e-9
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_diverg_beta_jsd_symmetric():
|
|
19
|
+
pa = {1: 0.9, 2: 0.1}
|
|
20
|
+
pb = {1: 0.5, 2: 0.5}
|
|
21
|
+
js_ab = diverg_beta(pa, pb, beta=0.5)
|
|
22
|
+
js_ba = diverg_beta(pb, pa, beta=0.5)
|
|
23
|
+
assert abs(js_ab - js_ba) < 1e-9 # JSD 对称
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_diverg_beta_reverse_kl_asymmetric():
|
|
27
|
+
pa = {1: 0.9, 2: 0.1}
|
|
28
|
+
pb = {1: 0.5, 2: 0.5}
|
|
29
|
+
rk_ab = diverg_beta(pa, pb, beta=1.0)
|
|
30
|
+
rk_ba = diverg_beta(pb, pa, beta=1.0)
|
|
31
|
+
assert abs(rk_ab - rk_ba) > 1e-6 # reverse KL 非对称
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_consensus_triggered_with_disagreement():
|
|
35
|
+
# 3/5 同意 → frac=0.6≥0.5 且存在分歧 → 触发
|
|
36
|
+
ans = ["液环禁向量", "液环禁向量", "液环禁向量", "用embedding", "别禁向量"]
|
|
37
|
+
rep = consensus_self_check(ans, tau=0.5)
|
|
38
|
+
assert rep["triggered"] is True
|
|
39
|
+
assert abs(rep["frac"] - 0.6) < 1e-9
|
|
40
|
+
assert rep["pseudo_answer"] == "液环禁向量"
|
|
41
|
+
assert rep["n"] == 5
|
|
42
|
+
assert rep["mean_divergence"] >= 0.0
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_consensus_not_triggered_all_same():
|
|
46
|
+
# 全同意 → 𝒴⁻=∅ → 不触发
|
|
47
|
+
ans = ["same"] * 6
|
|
48
|
+
rep = consensus_self_check(ans, tau=0.5)
|
|
49
|
+
assert rep["triggered"] is False
|
|
50
|
+
assert rep["frac"] == 1.0
|
|
51
|
+
assert rep["mean_divergence"] == 0.0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_consensus_not_triggered_low_frac():
|
|
55
|
+
# 票比 < τ → 不触发
|
|
56
|
+
ans = ["x", "y", "z", "w"]
|
|
57
|
+
rep = consensus_self_check(ans, tau=0.5)
|
|
58
|
+
assert rep["triggered"] is False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_consensus_empty():
|
|
62
|
+
rep = consensus_self_check([], tau=0.5)
|
|
63
|
+
assert rep["triggered"] is False
|
|
64
|
+
assert rep["n"] == 0
|
|
65
|
+
assert rep["pseudo_answer"] is None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_string_divergence_identical_zero():
|
|
69
|
+
assert string_divergence("液环 禁 向量", "液环 禁 向量") == 0.0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_consensus_tight_pseudo_low_divergence():
|
|
73
|
+
# 同意集高度一致 → mean_divergence 应远小于分歧大的情况
|
|
74
|
+
ans_tight = ["液环禁向量", "液环禁向量", "液环禁向量", "不同答案"]
|
|
75
|
+
rep_tight = consensus_self_check(ans_tight, tau=0.5)
|
|
76
|
+
assert rep_tight["triggered"] is True
|
|
77
|
+
assert rep_tight["mean_divergence"] < 1e-6 # 同意集完全相同 → 散度为0
|
|
@@ -72,3 +72,26 @@ def test_find_duplicates_detects_real_duplicate():
|
|
|
72
72
|
agent_id="marvis"))
|
|
73
73
|
r = st.find_duplicates(threshold=0.8)
|
|
74
74
|
assert any(c["similarity"] >= 1.0 for c in r["candidates"])
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ── v1.8.3 consensus 保护(Palantir owned 细化):consensus 结晶证据禁止单边 supersede ──
|
|
78
|
+
def test_supersede_consensus_protected():
|
|
79
|
+
st = WorkspaceState()
|
|
80
|
+
st.evidences = [
|
|
81
|
+
Evidence(id="c1", content="共识事实", agent_id="vera"),
|
|
82
|
+
Evidence(id="c2", content="共识事实", agent_id="marvis"),
|
|
83
|
+
Evidence(id="w1", content="取代者", agent_id="vera"),
|
|
84
|
+
]
|
|
85
|
+
# 构造 consensus 结晶:c1+c2 跨 agent 一致
|
|
86
|
+
st.memories = [
|
|
87
|
+
Memory(id="m1", content="共识事实", scope="consensus",
|
|
88
|
+
evidence_ids=["c1", "c2"], contributors=["vera", "marvis"]),
|
|
89
|
+
]
|
|
90
|
+
# loser 是 consensus 证据 → 拒绝
|
|
91
|
+
r = st.supersede_evidence("c1", "w1")
|
|
92
|
+
assert not r.get("ok")
|
|
93
|
+
assert "consensus evidence requires unanimous dissolution" in r.get("error", "")
|
|
94
|
+
# 非 consensus 证据仍可 supersede
|
|
95
|
+
st.evidences.append(Evidence(id="p1", content="普通证据", agent_id="vera"))
|
|
96
|
+
r = st.supersede_evidence("p1", "w1")
|
|
97
|
+
assert r.get("ok"), r
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|