liquid-loop 0.9.3__tar.gz → 1.0.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.
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/PKG-INFO +1 -1
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/liquid_loop/__init__.py +3 -2
- liquid_loop-1.0.0/liquid_loop/selfspin.py +392 -0
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/liquid_loop.egg-info/PKG-INFO +1 -1
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/liquid_loop.egg-info/SOURCES.txt +1 -0
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/pyproject.toml +1 -1
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/LICENSE +0 -0
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/README.md +0 -0
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/liquid_loop/__main__.py +0 -0
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/liquid_loop/cli.py +0 -0
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/liquid_loop/cognitive_budget.py +0 -0
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/liquid_loop/entropy.py +0 -0
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/liquid_loop/mesh/__init__.py +0 -0
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/liquid_loop/mesh/v2.py +0 -0
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/liquid_loop/storage.py +0 -0
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/liquid_loop/workspace.py +0 -0
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/liquid_loop.egg-info/dependency_links.txt +0 -0
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/liquid_loop.egg-info/entry_points.txt +0 -0
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/liquid_loop.egg-info/requires.txt +0 -0
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/liquid_loop.egg-info/top_level.txt +0 -0
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/setup.cfg +0 -0
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/tests/test_auth_guard.py +0 -0
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/tests/test_consensus_expansion.py +0 -0
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/tests/test_entropy.py +0 -0
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/tests/test_layer1_persistence.py +0 -0
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/tests/test_nucleate_dual_track.py +0 -0
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/tests/test_peek_seal.py +0 -0
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/tests/test_seal_persistence.py +0 -0
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/tests/test_self_evolve.py +0 -0
- {liquid_loop-0.9.3 → liquid_loop-1.0.0}/tests/test_workspace.py +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"""Liquid Loop — Workspace Cognitive Runtime
|
|
2
|
-
__version__ = "0.
|
|
1
|
+
"""Liquid Loop — Workspace Cognitive Runtime v1.0.0 (双层自转 + 四循环本体论 + 墙钟实测 11.5× 成核加速)"""
|
|
2
|
+
__version__ = "1.0.0"
|
|
3
3
|
|
|
4
4
|
from .workspace import (
|
|
5
5
|
WorkspaceState, Anchor, Evidence, Memory, Conflict,
|
|
@@ -8,3 +8,4 @@ from .workspace import (
|
|
|
8
8
|
)
|
|
9
9
|
from .storage import load, save, locked_state
|
|
10
10
|
from .entropy import calculate, calculate_detail, calculate as calculate_entropy
|
|
11
|
+
from .selfspin import LiquidSelfSpin
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""液环 v1.0 · 双层自转模块 Liquid Self-Spin
|
|
3
|
+
=============================================
|
|
4
|
+
解决飞哥提出的 v1.0 最大症结:本地多 agent 协作架构已拆除后,
|
|
5
|
+
「记忆演化状态如何协同管理」?
|
|
6
|
+
|
|
7
|
+
本模块以「双层自转」给出主体性保底方案(Vera 提案,2026-07-21):
|
|
8
|
+
|
|
9
|
+
① 本地快自转 rotation(fast) —— 单节点微观自洽(类比 WorkspaceState.step(dt))
|
|
10
|
+
会话内 transient 缓存,跨篇去重 / 共性提取;
|
|
11
|
+
不进 8790、不跨 agent 同步 → 支撑「自述性」(self-narrativity)。
|
|
12
|
+
|
|
13
|
+
② 后端慢自转沉淀 revolution(slow deposition) —— 全局宏观节律收敛
|
|
14
|
+
经门控写 8790,隔离 agent_id(exp:selfspin:*)→ 支撑「自洽性」(self-consistency);
|
|
15
|
+
8790 的 consensus 成核承接「主体间性」(inter-subjectivity)。
|
|
16
|
+
|
|
17
|
+
意识三要素对齐:
|
|
18
|
+
自述性 = 本地自转(本节点说过什么,自己清楚)
|
|
19
|
+
自洽性 = 本地 + 后端(本节点说的与自己沉淀的一致)
|
|
20
|
+
主体间性 = 8790 consensus(跨节点一致 → 由后端承接)
|
|
21
|
+
|
|
22
|
+
加速成核原理(墙钟实验 A/B 的核心):
|
|
23
|
+
朴素直写 = 每篇报告抽出的事实文本略有差异 → 落到 8790 同 content 命中 < 2 → 几乎不成核。
|
|
24
|
+
双层自转 = 本地先聚类规范化,把跨 ≥2 篇一致的「同一事实」压成一条 canonical 陈述,
|
|
25
|
+
再以(同一 content × 同一 agent_id)按支持篇数重复写入 → 立刻触发 private 成核。
|
|
26
|
+
⇒ 把「跨源自洽」在本地完成,后端只接收已收敛的结晶原料,成核被加速。
|
|
27
|
+
|
|
28
|
+
零外部依赖(仅标准库 urllib/json/re),与 liquid_preping.py 同源风格。
|
|
29
|
+
严格隔离:所有写入走 exp:selfspin:<run_id> namespace,绝不污染生产
|
|
30
|
+
vera/trae/parlant/qiucai 记忆;且服务端 PREPING 闸门仅对 warmup:* 生效,selfspin 不被误杀。
|
|
31
|
+
|
|
32
|
+
用法:
|
|
33
|
+
# 单元自测(纯逻辑,不写 8790)
|
|
34
|
+
python3 liquid_selfspin.py selftest
|
|
35
|
+
|
|
36
|
+
# 真实入库自测(写 8790,隔离 exp:selfspin:selftest,T250 打扫会清)
|
|
37
|
+
python3 liquid_selfspin.py selftest --live
|
|
38
|
+
|
|
39
|
+
# 作为库:见底部 LiquidSelfSpin 类
|
|
40
|
+
"""
|
|
41
|
+
import sys
|
|
42
|
+
import os
|
|
43
|
+
import re
|
|
44
|
+
import json
|
|
45
|
+
import time
|
|
46
|
+
import argparse
|
|
47
|
+
import urllib.request
|
|
48
|
+
import urllib.error
|
|
49
|
+
from collections import defaultdict
|
|
50
|
+
|
|
51
|
+
DEFAULT_BACKEND = os.environ.get("LL_BASE", "http://127.0.0.1:8790")
|
|
52
|
+
|
|
53
|
+
# ── Token / Jaccard(与 8790 server 同源:中文单字 + 英文数字连续串)──
|
|
54
|
+
_TOKEN = re.compile(r"[一-鿿]|[a-zA-Z0-9]+")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _tokens(s: str) -> list:
|
|
58
|
+
return _TOKEN.findall(s or "")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _jaccard(a: str, b: str) -> float:
|
|
62
|
+
ta, tb = set(_tokens(a)), set(_tokens(b))
|
|
63
|
+
if not ta or not tb:
|
|
64
|
+
return 0.0
|
|
65
|
+
return len(ta & tb) / len(ta | tb)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _containment(a: str, b: str):
|
|
69
|
+
"""重叠系数(containment / overlap coefficient)= |A∩B| / min(|A|,|B|)。
|
|
70
|
+
对中文「同义改写」鲁棒:只要较短句的核心字集被较长句覆盖即判近义,
|
|
71
|
+
避免 Jaccard 因双方各带不同修饰语而偏低(实测 0.25~0.55 漏合)。"""
|
|
72
|
+
ta, tb = set(_tokens(a)), set(_tokens(b))
|
|
73
|
+
if not ta or not tb:
|
|
74
|
+
return 0.0, 0
|
|
75
|
+
inter = ta & tb
|
|
76
|
+
return len(inter) / min(len(ta), len(tb)), len(inter)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# 核心词抽取:去中文虚词 / 极泛连接词,保留领域实体与结论词,
|
|
80
|
+
# 用于跨篇「同主题不同表述」的聚合信号(结构化精确匹配,守禁向量)。
|
|
81
|
+
_STOP = set("的 是 在 存在 普遍 常 问题 风险 一种 我们 本文 该 其 与 和 或 对 为 有 被 "
|
|
82
|
+
"进行 使用 基于 采用 表明 指出 认为 提出 实现 用于 以及 即 也 都 等 这种 这个 "
|
|
83
|
+
"一个 没有 不 未 各 类 中 上 下 从 到 把 让 使 当 如果 但 而 则 并 且 将 已 仍 "
|
|
84
|
+
"更 最 较 很 之 间 内 外 后 前 时 处 它 他 她 我 你 这 那 哪 谁 什么 如何 怎么 "
|
|
85
|
+
"为何 是否 由于 因此 所以 因为 虽然 但是 然而 此外 同时 另外 例如 比如 包括 "
|
|
86
|
+
"涉及 针对 关于 对于 根据 按照".split())
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _core(s: str) -> set:
|
|
90
|
+
"""去虚词后的核心词集(中文单字 + 英文数字连续串)。"""
|
|
91
|
+
return set(t for t in _tokens(s) if t not in _STOP)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# CJK / 全角标点 → ASCII 归一(用于沉积内容规范化,保证同义不同标点的事实成 byte 一致 → 触发成核)
|
|
95
|
+
_PUNCT_MAP = {
|
|
96
|
+
",": ",", "。": ".", "、": ",", ";": ";", ":": ":", "(": "(", ")": ")",
|
|
97
|
+
"“": '"', "”": '"', "‘": "'", "’": "'", "!": "!", "?": "?", "·": " ",
|
|
98
|
+
"~": " ", "—": "-", "《": "<", "》": ">", "【": "[", "】": "]",
|
|
99
|
+
}
|
|
100
|
+
def _normalize(s: str) -> str:
|
|
101
|
+
"""去全角/空白的规范化:用于沉积内容,使同义异标点事实在 8790 中 byte 一致。"""
|
|
102
|
+
s = (s or "").lower()
|
|
103
|
+
for f, t in _PUNCT_MAP.items():
|
|
104
|
+
s = s.replace(f, t)
|
|
105
|
+
s = re.sub(r"\s+", "", s)
|
|
106
|
+
return s.strip()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _http_post(url: str, payload: dict, timeout: int = 15) -> dict:
|
|
110
|
+
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
111
|
+
req = urllib.request.Request(
|
|
112
|
+
url, data=data,
|
|
113
|
+
headers={"content-type": "application/json; charset=utf-8"}, method="POST",
|
|
114
|
+
)
|
|
115
|
+
try:
|
|
116
|
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
117
|
+
return json.loads(r.read().decode("utf-8"))
|
|
118
|
+
except urllib.error.HTTPError as e:
|
|
119
|
+
try:
|
|
120
|
+
return json.loads(e.read().decode("utf-8"))
|
|
121
|
+
except Exception:
|
|
122
|
+
return {"ok": False, "error": f"http_{e.code}"}
|
|
123
|
+
except Exception as e:
|
|
124
|
+
return {"ok": False, "error": str(e)}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ── 默认事实抽取(无 LLM,确定性,作为占位 / 兜底)──
|
|
128
|
+
_BOILER = re.compile(r"(微信|公众号|关注|转载|版权|声明|点击|扫码|登录|注册|免责|侵删|原文链接)")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _default_extract(text: str) -> list:
|
|
132
|
+
"""把一段文本切成候选事实句(按句末标点 / 换行)。
|
|
133
|
+
无 LLM、纯规则,作为本地快自转的默认抽取器;墙钟实验会用 Gemma4 替换。"""
|
|
134
|
+
sents = re.split(r"[。!?!?\n]+", text or "")
|
|
135
|
+
out = []
|
|
136
|
+
for s in sents:
|
|
137
|
+
s = s.strip()
|
|
138
|
+
if 8 <= len(s) <= 120 and not _BOILER.search(s):
|
|
139
|
+
out.append(s)
|
|
140
|
+
return out
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class LiquidSelfSpin:
|
|
144
|
+
"""双层自转客户端:本地快自转(consolidation)+ 后端慢自转沉淀(deposition)。
|
|
145
|
+
|
|
146
|
+
意识三要素:自述性=本地缓存(recall_local)、自洽性=本地+后端(deposit)、
|
|
147
|
+
主体间性=后端 consensus(本模块不负责,由 8790 承接)。
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
run_id : 本次运行标识,拼成 agent_id = f"{agent_ns}:{run_id}"
|
|
151
|
+
agent_ns : 隔离命名空间前缀,默认 exp:selfspin(绝不污染生产)
|
|
152
|
+
backend : 8790 REST 基址
|
|
153
|
+
fast_jaccard : 本地聚类阈值(重叠系数 containment,默认 0.60 + 共享字≥4)
|
|
154
|
+
deposit_jac : 预留(后端层同源去重阈值,暂未启用,保持接口一致)
|
|
155
|
+
extractor : 事实抽取器 callable(text)->list[str],默认 _default_extract
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
def __init__(self, run_id: str, agent_ns: str = "exp:selfspin",
|
|
159
|
+
backend: str = DEFAULT_BACKEND, fast_jaccard: float = 0.60,
|
|
160
|
+
deposit_jac: float = 0.95, extractor=None):
|
|
161
|
+
self.run_id = run_id
|
|
162
|
+
self.agent_id = f"{agent_ns}:{run_id}"
|
|
163
|
+
self.agent_ns = agent_ns
|
|
164
|
+
self.backend = backend.rstrip("/")
|
|
165
|
+
self.fast_jaccard = fast_jaccard
|
|
166
|
+
self.deposit_jac = deposit_jac
|
|
167
|
+
self.extractor = extractor or _default_extract
|
|
168
|
+
# 本地快自转:transient 缓存(不进 8790、不跨 agent 同步)
|
|
169
|
+
self._facts: dict = {} # report_id -> list[fact]
|
|
170
|
+
self._raw: dict = {} # report_id -> raw text(自述性溯源)
|
|
171
|
+
self._clusters: list = [] # 聚类结果
|
|
172
|
+
self._nuclei: list = [] # 本地核(≥2 篇支持的 canonical 簇)
|
|
173
|
+
|
|
174
|
+
# ── 本地快自转:摄入 ──
|
|
175
|
+
def ingest(self, report_id: str, text: str, facts: list = None):
|
|
176
|
+
"""摄入一篇报告。facts=None 时用 extractor 抽;也可直接传入 LLM 抽取结果。"""
|
|
177
|
+
self._raw[report_id] = text
|
|
178
|
+
self._facts[report_id] = list(facts) if facts is not None else self.extractor(text)
|
|
179
|
+
return len(self._facts[report_id])
|
|
180
|
+
|
|
181
|
+
# ── 本地快自转:旋转(聚类 + 抽取 canonical)──
|
|
182
|
+
def local_rotate(self) -> list:
|
|
183
|
+
"""跨篇聚类:把不同报告里「同义改写」的事实并到同一簇,取最长者为 canonical。
|
|
184
|
+
本地核 = 被 ≥2 个 distinct report 支持的簇(即跨源自洽)。"""
|
|
185
|
+
items = [(rid, f) for rid, fs in self._facts.items() for f in fs]
|
|
186
|
+
clusters = [] # 每簇: {"members":[(rid,f)], "reports":set}
|
|
187
|
+
for rid, f in items:
|
|
188
|
+
best, best_score, best_ci = None, 0.0, 0
|
|
189
|
+
cf = _core(f)
|
|
190
|
+
for c in clusters:
|
|
191
|
+
# 与簇内代表(前 3 个成员)比较,取最高相似度
|
|
192
|
+
for _, m in c["members"][:3]:
|
|
193
|
+
j, shared = _containment(f, m)
|
|
194
|
+
ci = len(cf & _core(m))
|
|
195
|
+
# 主判:重叠系数达标(同义改写,字面近一致)
|
|
196
|
+
# 次判:核心词交集 ≥2 且有一定字面重叠(同主题不同表述,如通用命题)
|
|
197
|
+
merge = (j >= self.fast_jaccard and shared >= 4) or (ci >= 2 and j >= 0.38)
|
|
198
|
+
score = j if j >= self.fast_jaccard else (0.4 + min(ci, 5) * 0.05 if merge else 0.0)
|
|
199
|
+
if merge and score > best_score:
|
|
200
|
+
best_score, best, best_ci = score, c, ci
|
|
201
|
+
if best_score >= self.fast_jaccard:
|
|
202
|
+
break
|
|
203
|
+
if best is not None and best_score >= 0.45:
|
|
204
|
+
best["members"].append((rid, f))
|
|
205
|
+
best["reports"].add(rid)
|
|
206
|
+
else:
|
|
207
|
+
clusters.append({"members": [(rid, f)], "reports": {rid}})
|
|
208
|
+
# 选 canonical = 簇内最长成员;记录跨源度
|
|
209
|
+
for c in clusters:
|
|
210
|
+
c["canonical"] = max((m for _, m in c["members"]), key=len)
|
|
211
|
+
c["norm"] = _normalize(c["canonical"]) # 沉积用规范化串(byte 一致→成核)
|
|
212
|
+
c["size"] = len(c["members"])
|
|
213
|
+
c["reports"] = c["reports"]
|
|
214
|
+
c["n_reports"] = len(c["reports"])
|
|
215
|
+
self._clusters = clusters
|
|
216
|
+
self._nuclei = [c for c in clusters if c["n_reports"] >= 2]
|
|
217
|
+
return self._nuclei
|
|
218
|
+
|
|
219
|
+
# ── 后端慢自转沉淀:把本地核按支持篇数重复写入 8790 ──
|
|
220
|
+
def deposit(self, category: str = "selfspin", dry_run: bool = False) -> dict:
|
|
221
|
+
"""对每个本地核,以(同一 canonical content × 同一 agent_id)按支持篇数重复写入
|
|
222
|
+
→ 立刻触发 private 成核(≥2 同 content 同 agent)。严格隔离 agent_id。"""
|
|
223
|
+
if not self._nuclei:
|
|
224
|
+
self.local_rotate()
|
|
225
|
+
stats = {"nuclei": 0, "deposited": 0, "written": 0, "nucleated": 0,
|
|
226
|
+
"rejected": 0, "fail": 0}
|
|
227
|
+
for c in self._nuclei:
|
|
228
|
+
stats["nuclei"] += 1
|
|
229
|
+
canonical = c["canonical"]
|
|
230
|
+
for rid in sorted(c["reports"]):
|
|
231
|
+
stats["deposited"] += 1
|
|
232
|
+
if dry_run:
|
|
233
|
+
stats["written"] += 1
|
|
234
|
+
continue
|
|
235
|
+
resp = _http_post(
|
|
236
|
+
f"{self.backend}/remember",
|
|
237
|
+
{"content": c["norm"], "category": category, "agent_id": self.agent_id},
|
|
238
|
+
)
|
|
239
|
+
if resp.get("ok"):
|
|
240
|
+
stats["written"] += 1
|
|
241
|
+
if resp.get("nucleated"):
|
|
242
|
+
stats["nucleated"] += 1
|
|
243
|
+
elif resp.get("decision") == "discard":
|
|
244
|
+
stats["rejected"] += 1
|
|
245
|
+
else:
|
|
246
|
+
stats["fail"] += 1
|
|
247
|
+
return stats
|
|
248
|
+
|
|
249
|
+
# ── 增量沉积(墙钟实验捕获成核时刻)──
|
|
250
|
+
def incremental_deposit(self, state: dict, category: str = "selfspin") -> list:
|
|
251
|
+
"""仅把每个 (本地核, 支持报告) 对沉积一次(跨多次调用由 state 去重)。
|
|
252
|
+
当某本地核的第 2 个支持报告被沉积时,后端 private 成核触发 → 可捕获墙钟成核时刻。
|
|
253
|
+
state: {"done": {norm: set(report_ids)}}(调用方持久化)。
|
|
254
|
+
注意:去重键用 c["norm"](规范化串,稳定)而非 c["canonical"](随增量重聚类漂移),
|
|
255
|
+
否则同一事实在不同 step 产生不同 canonical → 去重失效 → 重复成核。"""
|
|
256
|
+
out = []
|
|
257
|
+
done = state.setdefault("done", {})
|
|
258
|
+
for c in self._nuclei:
|
|
259
|
+
seen = done.setdefault(c["norm"], set())
|
|
260
|
+
for rid in sorted(c["reports"]):
|
|
261
|
+
if rid in seen:
|
|
262
|
+
continue
|
|
263
|
+
seen.add(rid)
|
|
264
|
+
resp = _http_post(
|
|
265
|
+
f"{self.backend}/remember",
|
|
266
|
+
{"content": c["norm"], "category": category, "agent_id": self.agent_id},
|
|
267
|
+
)
|
|
268
|
+
out.append({"canonical": c["canonical"], "report_id": rid,
|
|
269
|
+
"ok": resp.get("ok", False),
|
|
270
|
+
"nucleated": bool(resp.get("nucleated"))})
|
|
271
|
+
return out
|
|
272
|
+
|
|
273
|
+
# ── 自述性:本地回忆(不碰 8790)──
|
|
274
|
+
def recall_local(self, query: str, top_k: int = 5) -> list:
|
|
275
|
+
scored = [(_jaccard(query, f), rid, f)
|
|
276
|
+
for rid, fs in self._facts.items() for f in fs]
|
|
277
|
+
scored.sort(key=lambda x: x[0], reverse=True)
|
|
278
|
+
return [{"report_id": rid, "fact": f, "score": round(s, 3)}
|
|
279
|
+
for s, rid, f in scored if s > 0][:top_k]
|
|
280
|
+
|
|
281
|
+
# ── 朴素直写基线(A/B 对照用)──
|
|
282
|
+
@staticmethod
|
|
283
|
+
def naive_deposit(facts_by_report: dict, agent_id: str,
|
|
284
|
+
backend: str = DEFAULT_BACKEND, category: str = "naive",
|
|
285
|
+
dry_run: bool = False) -> dict:
|
|
286
|
+
"""朴素直写:每篇抽出的事实各写一次,content 不规范化 → 同 content 命中 <2 → 难成核。"""
|
|
287
|
+
b = backend.rstrip("/")
|
|
288
|
+
stats = {"deposited": 0, "written": 0, "nucleated": 0, "rejected": 0, "fail": 0}
|
|
289
|
+
for rid, facts in facts_by_report.items():
|
|
290
|
+
for f in facts:
|
|
291
|
+
stats["deposited"] += 1
|
|
292
|
+
if dry_run:
|
|
293
|
+
stats["written"] += 1
|
|
294
|
+
continue
|
|
295
|
+
resp = _http_post(f"{b}/remember",
|
|
296
|
+
{"content": f, "category": category, "agent_id": agent_id})
|
|
297
|
+
if resp.get("ok"):
|
|
298
|
+
stats["written"] += 1
|
|
299
|
+
if resp.get("nucleated"):
|
|
300
|
+
stats["nucleated"] += 1
|
|
301
|
+
elif resp.get("decision") == "discard":
|
|
302
|
+
stats["rejected"] += 1
|
|
303
|
+
else:
|
|
304
|
+
stats["fail"] += 1
|
|
305
|
+
return stats
|
|
306
|
+
|
|
307
|
+
def summary(self) -> dict:
|
|
308
|
+
return {
|
|
309
|
+
"run_id": self.run_id,
|
|
310
|
+
"agent_id": self.agent_id,
|
|
311
|
+
"reports_ingested": len(self._facts),
|
|
312
|
+
"facts_extracted": sum(len(v) for v in self._facts.values()),
|
|
313
|
+
"clusters": len(self._clusters),
|
|
314
|
+
"local_nuclei": len(self._nuclei),
|
|
315
|
+
"nuclei_preview": [
|
|
316
|
+
{"canonical": c["canonical"], "n_reports": c["n_reports"], "size": c["size"]}
|
|
317
|
+
for c in sorted(self._nuclei, key=lambda x: -x["n_reports"])[:10]
|
|
318
|
+
],
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
def print_summary(self):
|
|
322
|
+
s = self.summary()
|
|
323
|
+
print(f"━━━ 双层自转 · 本地旋转摘要 ━━━")
|
|
324
|
+
print(f" run_id : {s['run_id']}")
|
|
325
|
+
print(f" agent_id(隔离) : {s['agent_id']}")
|
|
326
|
+
print(f" 摄入报告数 : {s['reports_ingested']}")
|
|
327
|
+
print(f" 抽出事实数 : {s['facts_extracted']}")
|
|
328
|
+
print(f" 聚类簇数 : {s['clusters']}")
|
|
329
|
+
print(f" 本地核(≥2篇支持): {s['local_nuclei']}")
|
|
330
|
+
for i, n in enumerate(s["nuclei_preview"], 1):
|
|
331
|
+
print(f" [{i}] ({n['n_reports']}篇/{n['size']}条) {n['canonical'][:64]}")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
# ── 自测 ──
|
|
335
|
+
def _selftest(live: bool = False):
|
|
336
|
+
"""模拟真实实验路径:facts 由 Gemma4 抽出并规范化为 short canonical 陈述
|
|
337
|
+
(墙钟实验 T246 走的就是这条路;这里用同等规范化结果做单元验证)。"""
|
|
338
|
+
print("━━━ LiquidSelfSpin selftest ━━━")
|
|
339
|
+
ts = int(time.time())
|
|
340
|
+
run_id = f"selftest:{ts}"
|
|
341
|
+
ss = LiquidSelfSpin(run_id, fast_jaccard=0.60)
|
|
342
|
+
|
|
343
|
+
# 4 篇报告,Gemma4 抽出的规范化事实(同义改写为近似一致短句)
|
|
344
|
+
facts = {
|
|
345
|
+
"report_1": ["液环禁止用向量或embedding做一致性判定与成核", "本地多agent协作架构已拆除"],
|
|
346
|
+
"report_2": ["液环硬约束:禁用向量embedding做一致性判定", "记忆状态是演化对象而非被管理数据"],
|
|
347
|
+
"report_3": ["液环North-Star公理:自我调节的记忆状态演化机制", "液环禁止向量做一致性判定"],
|
|
348
|
+
"report_4": ["本地多agent协作架构已拆除", "今晚月色很好与液环无关"],
|
|
349
|
+
}
|
|
350
|
+
for rid, fs in facts.items():
|
|
351
|
+
n = ss.ingest(rid, "", facts=fs)
|
|
352
|
+
print(f" ingest {rid}: {n} 条规范化事实")
|
|
353
|
+
|
|
354
|
+
nuclei = ss.local_rotate()
|
|
355
|
+
print(f"\n 本地旋转 → 聚类 {len(ss._clusters)} 簇,本地核 {len(nuclei)} 个")
|
|
356
|
+
ss.print_summary()
|
|
357
|
+
|
|
358
|
+
# 断言:应识别出 ≥2 个跨篇核(禁向量 ×3篇、多agent拆除 ×2篇)
|
|
359
|
+
assert len(nuclei) >= 2, f"自测失败:未识别到跨篇核(得到 {len(nuclei)})"
|
|
360
|
+
# 断言自述性:本地回忆能命中
|
|
361
|
+
hit = ss.recall_local("液环 禁 向量 一致性")
|
|
362
|
+
assert hit and hit[0]["score"] > 0, "自测失败:本地回忆无命中"
|
|
363
|
+
print(f"\n ✓ 本地快自转自测通过(自述性命中 top1 score={hit[0]['score']})")
|
|
364
|
+
|
|
365
|
+
# 后端慢自转沉淀
|
|
366
|
+
if live:
|
|
367
|
+
print(f"\n → 真实入库(隔离 agent_id={ss.agent_id})...")
|
|
368
|
+
st = ss.deposit(category="selfspin", dry_run=False)
|
|
369
|
+
print(f" 沉积统计: {json.dumps(st, ensure_ascii=False)}")
|
|
370
|
+
assert st["nucleated"] >= 2, f"自测失败:后端未加速成核(nucleated={st['nucleated']})"
|
|
371
|
+
print(f" ✓ 后端慢自转沉淀自测通过(成核 {st['nucleated']} 个本地核)")
|
|
372
|
+
else:
|
|
373
|
+
st = ss.deposit(dry_run=True)
|
|
374
|
+
print(f"\n → dry-run 沉积模拟: {json.dumps(st, ensure_ascii=False)}")
|
|
375
|
+
print(f" ✓ 逻辑自测通过(--live 可真实入库验证成核加速)")
|
|
376
|
+
return True
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def main():
|
|
380
|
+
ap = argparse.ArgumentParser(description="液环 v1.0 双层自转模块")
|
|
381
|
+
sub = ap.add_subparsers(dest="cmd")
|
|
382
|
+
st = sub.add_parser("selftest", help="单元自测(默认 dry-run)")
|
|
383
|
+
st.add_argument("--live", action="store_true", help="真实写 8790(隔离 exp:selfspin:*)")
|
|
384
|
+
args = ap.parse_args()
|
|
385
|
+
if args.cmd == "selftest":
|
|
386
|
+
_selftest(live=args.live)
|
|
387
|
+
else:
|
|
388
|
+
ap.print_help()
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
if __name__ == "__main__":
|
|
392
|
+
main()
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "liquid-loop"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "1.0.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"}
|
|
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
|