mindforge-memory 6.4.1__py3-none-any.whl
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.
- MindForge.py +20 -0
- mindforge/__init__.py +111 -0
- mindforge/adapters/__init__.py +18 -0
- mindforge/adapters/claude_adapter.py +84 -0
- mindforge/adapters/crewai_memory.py +76 -0
- mindforge/adapters/generic_api.py +183 -0
- mindforge/adapters/langgraph_store.py +463 -0
- mindforge/adapters/openclaw_adapter.py +91 -0
- mindforge/api/__init__.py +1 -0
- mindforge/api/server.py +1567 -0
- mindforge/cli/__init__.py +1 -0
- mindforge/cli/main.py +13607 -0
- mindforge/core/__init__.py +37 -0
- mindforge/core/embedding.py +847 -0
- mindforge/core/encryption.py +597 -0
- mindforge/core/indexer.py +363 -0
- mindforge/core/mindforge.py +6298 -0
- mindforge/core/paths.py +43 -0
- mindforge/core/query.py +501 -0
- mindforge/core/storage.py +18656 -0
- mindforge/core/types.py +314 -0
- mindforge/core/version.py +56 -0
- mindforge/daemon/__init__.py +0 -0
- mindforge/daemon/server.py +277 -0
- mindforge/mcp/__init__.py +4 -0
- mindforge/mcp/server.py +2559 -0
- mindforge/modules/__init__.py +138 -0
- mindforge/modules/categorizer.py +151 -0
- mindforge/modules/conflict_detector.py +356 -0
- mindforge/modules/connectors.py +331 -0
- mindforge/modules/context_window.py +337 -0
- mindforge/modules/event_bus.py +572 -0
- mindforge/modules/evolution.py +210 -0
- mindforge/modules/experience.py +402 -0
- mindforge/modules/federated.py +722 -0
- mindforge/modules/federated_acl.py +393 -0
- mindforge/modules/follow_up.py +315 -0
- mindforge/modules/hybrid_search.py +565 -0
- mindforge/modules/importer.py +374 -0
- mindforge/modules/integrator.py +196 -0
- mindforge/modules/intent_router.py +359 -0
- mindforge/modules/knowledge_graph.py +395 -0
- mindforge/modules/memory_compressor.py +592 -0
- mindforge/modules/memory_decay.py +474 -0
- mindforge/modules/memory_passport.py +250 -0
- mindforge/modules/multi_agent.py +534 -0
- mindforge/modules/multimodal.py +304 -0
- mindforge/modules/outcome_learning.py +186 -0
- mindforge/modules/personality.py +275 -0
- mindforge/modules/principled_forgetting.py +258 -0
- mindforge/modules/privacy.py +736 -0
- mindforge/modules/recall.py +263 -0
- mindforge/modules/session_focus.py +305 -0
- mindforge/modules/share_conflict.py +521 -0
- mindforge/modules/skill_extractor.py +340 -0
- mindforge/modules/skill_pack.py +205 -0
- mindforge/modules/timeline_viz.py +206 -0
- mindforge/modules/v6.py +931 -0
- mindforge_memory-6.4.1.dist-info/METADATA +281 -0
- mindforge_memory-6.4.1.dist-info/RECORD +64 -0
- mindforge_memory-6.4.1.dist-info/WHEEL +5 -0
- mindforge_memory-6.4.1.dist-info/entry_points.txt +3 -0
- mindforge_memory-6.4.1.dist-info/licenses/LICENSE +21 -0
- mindforge_memory-6.4.1.dist-info/top_level.txt +2 -0
mindforge/modules/v6.py
ADDED
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MindForge 扩展端口(v6.0.0 已发布)
|
|
3
|
+
====================================
|
|
4
|
+
|
|
5
|
+
v6.0.0 的两个旗舰特性(多 Agent 协作、6G AI 记忆预览)已在正式版落地:
|
|
6
|
+
|
|
7
|
+
1. **多 Agent 协作**:`MultiAgentAdapterPort` 把 `modules/multi_agent.py`
|
|
8
|
+
的 `MultiAgentMemoryManager`(三级角色 owner/editor/reader、
|
|
9
|
+
last-write-wins 冲突仲裁、PRIVATE/STRICT 隐私护栏)挂到 v6 端口上,
|
|
10
|
+
状态为 AVAILABLE。通过 `get_v6_port(V6Capability.MULTI_AGENT_COLLABORATION,
|
|
11
|
+
manager)` 传入既有管理器实例即可使用;facade 侧 `MindForge.multi_agent`
|
|
12
|
+
已直接暴露该管理器。
|
|
13
|
+
|
|
14
|
+
2. **6G AI 记忆预览**:`MemoryPreviewPort` 提供与稳定版并行的实验分支——
|
|
15
|
+
Preview Channel(独立 SQLite 预览库,不污染主库)、上下文蒸馏
|
|
16
|
+
(按重要度分层 + Token 预算压缩)、增量索引统计(写入即索引的近实时
|
|
17
|
+
召回指标)与单记忆时间线。状态为 AVAILABLE。
|
|
18
|
+
|
|
19
|
+
状态语义
|
|
20
|
+
--------
|
|
21
|
+
- `PLANNED` :仅规划,无实现
|
|
22
|
+
- `EXPERIMENTAL` :有实验性实现,API 可能变更
|
|
23
|
+
- `AVAILABLE` :正式发布,可生产使用
|
|
24
|
+
|
|
25
|
+
用法示例
|
|
26
|
+
--------
|
|
27
|
+
>>> from mindforge.modules import v6_status, get_v6_port, V6Capability
|
|
28
|
+
>>> v6_status()["released"]
|
|
29
|
+
True
|
|
30
|
+
>>> port = get_v6_port(V6Capability.MEMORY_PREVIEW, storage)
|
|
31
|
+
>>> port.is_available()
|
|
32
|
+
True
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
import json
|
|
36
|
+
import os
|
|
37
|
+
import sqlite3
|
|
38
|
+
import time
|
|
39
|
+
import uuid
|
|
40
|
+
from abc import ABC, abstractmethod
|
|
41
|
+
from enum import Enum
|
|
42
|
+
from typing import Any, Dict, List, Optional, Type
|
|
43
|
+
|
|
44
|
+
from mindforge.core.version import __version__
|
|
45
|
+
# v6.1.2:memory_id 入参归一(接受 str 或 add() 返回的 MemoryEntry 对象)
|
|
46
|
+
from mindforge.core.types import coerce_memory_id
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# v6 常量
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
#: v6.0.0 目标版本号(当前已发布版本)
|
|
53
|
+
V6_TARGET_VERSION: str = "6.0.0"
|
|
54
|
+
|
|
55
|
+
#: 错误信息前缀
|
|
56
|
+
_V6_PREFIX: str = "MindForge v6.0.0"
|
|
57
|
+
|
|
58
|
+
#: Preview Channel 独立库的默认存放目录(相对数据目录)
|
|
59
|
+
_PREVIEW_DIRNAME: str = "preview_channels"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class V6Status(str, Enum):
|
|
63
|
+
"""v6 端口的成熟度状态(str 子类,便于直接 JSON 序列化)"""
|
|
64
|
+
|
|
65
|
+
PLANNED = "planned" # 仅规划,无可用实现
|
|
66
|
+
EXPERIMENTAL = "experimental" # 实验性实现,接口可能变更
|
|
67
|
+
AVAILABLE = "available" # 正式发布,可生产使用
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class V6Capability(str, Enum):
|
|
71
|
+
"""v6.0.0 规划中的能力枚举(与官网 #v6 区块一一对应)"""
|
|
72
|
+
|
|
73
|
+
#: 多 Agent 协作:共享记忆空间、角色权限、冲突仲裁
|
|
74
|
+
MULTI_AGENT_COLLABORATION = "multi_agent_collaboration"
|
|
75
|
+
#: 6G AI 记忆预览:Preview Channel、上下文蒸馏、增量索引、时间线可视化
|
|
76
|
+
MEMORY_PREVIEW = "memory_preview"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _estimate_tokens(text: str) -> int:
|
|
80
|
+
"""粗略估算文本的 Token 数(中英混合场景:每 1 字符约 0.5 Token)。
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
text: 输入文本
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
估算 Token 数(至少 1)
|
|
87
|
+
"""
|
|
88
|
+
if not text:
|
|
89
|
+
return 0
|
|
90
|
+
return max(1, int(len(text) * 0.5))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
# 端口抽象基类
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
class V6Port(ABC):
|
|
98
|
+
"""v6 扩展端口抽象基类
|
|
99
|
+
|
|
100
|
+
子类必须声明 `capability` 与 `STATUS`。业务方法由各具体端口定义,
|
|
101
|
+
未实现的能力抛 `NotImplementedError`,确保误调用立即暴露而不是静默出错。
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
#: 该端口对应的 v6 能力
|
|
105
|
+
capability: V6Capability
|
|
106
|
+
|
|
107
|
+
#: 端口成熟度(类级常量,便于不实例化即可查询)
|
|
108
|
+
STATUS: V6Status = V6Status.PLANNED
|
|
109
|
+
|
|
110
|
+
#: 是否必须带构造参数(无法无参实例化)
|
|
111
|
+
requires_args: bool = False
|
|
112
|
+
|
|
113
|
+
@abstractmethod
|
|
114
|
+
def status(self) -> V6Status:
|
|
115
|
+
"""返回端口当前成熟度状态。"""
|
|
116
|
+
raise NotImplementedError
|
|
117
|
+
|
|
118
|
+
def is_available(self) -> bool:
|
|
119
|
+
"""端口是否已可生产使用。"""
|
|
120
|
+
return self.status() is V6Status.AVAILABLE
|
|
121
|
+
|
|
122
|
+
def describe(self) -> Dict[str, Any]:
|
|
123
|
+
"""返回端口的自描述信息(供 CLI / MCP / 官网展示)。
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
包含 capability / status / available / port / target_version 的字典。
|
|
127
|
+
"""
|
|
128
|
+
return {
|
|
129
|
+
"capability": self.capability.value,
|
|
130
|
+
"status": self.status().value,
|
|
131
|
+
"available": self.is_available(),
|
|
132
|
+
"port": type(self).__name__,
|
|
133
|
+
"target_version": V6_TARGET_VERSION,
|
|
134
|
+
"requires_args": self.requires_args,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
def summary(self) -> Dict[str, Any]:
|
|
138
|
+
"""返回端口的简要运行概览;未实现时返回空字典。
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
端口自定义的概览字典,未实现时为空字典。
|
|
142
|
+
"""
|
|
143
|
+
return {}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
# 端口 1:多 Agent 协作(正式发布,经 MultiAgentAdapterPort 挂载)
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
class MultiAgentCollaborationPort(V6Port):
|
|
151
|
+
"""多 Agent 协作端口(接口规范)
|
|
152
|
+
|
|
153
|
+
对应 v6.0.0 的"多 Agent 协作"方向:多个 Agent 在同一本地库内共享记忆,
|
|
154
|
+
按角色隔离权限,并处理写入冲突。
|
|
155
|
+
|
|
156
|
+
正式实现:`MultiAgentAdapterPort` 把 `modules/multi_agent.py` 的
|
|
157
|
+
`MultiAgentMemoryManager`(三级角色 owner / editor / reader、
|
|
158
|
+
last-write-wins 冲突仲裁、PRIVATE / STRICT 隐私护栏)挂到本端口上,
|
|
159
|
+
状态为 AVAILABLE。
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
capability = V6Capability.MULTI_AGENT_COLLABORATION
|
|
163
|
+
STATUS = V6Status.AVAILABLE
|
|
164
|
+
|
|
165
|
+
def status(self) -> V6Status:
|
|
166
|
+
"""多 Agent 协作已随 v6.0.0 正式发布,返回 AVAILABLE。"""
|
|
167
|
+
return self.STATUS
|
|
168
|
+
|
|
169
|
+
# —— 空间管理 ——
|
|
170
|
+
|
|
171
|
+
def create_space(self, name: str, owner_agent: str,
|
|
172
|
+
description: str = "", policy: str = "shared") -> Dict[str, Any]:
|
|
173
|
+
"""创建共享记忆空间。"""
|
|
174
|
+
raise NotImplementedError(
|
|
175
|
+
f"{_V6_PREFIX}:请通过 get_v6_port(MULTI_AGENT_COLLABORATION, manager)"
|
|
176
|
+
f" 或 MindForge.multi_agent 使用正式实现。"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def list_spaces(self, agent_id: str = "") -> List[Dict[str, Any]]:
|
|
180
|
+
"""列出共享记忆空间。"""
|
|
181
|
+
raise NotImplementedError(
|
|
182
|
+
f"{_V6_PREFIX}:请通过 get_v6_port(MULTI_AGENT_COLLABORATION, manager)"
|
|
183
|
+
f" 或 MindForge.multi_agent 使用正式实现。"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# —— 成员与权限 ——
|
|
187
|
+
|
|
188
|
+
def add_member(self, space_ref: str, agent_id: str,
|
|
189
|
+
role: str = "reader", actor: str = "") -> Dict[str, Any]:
|
|
190
|
+
"""向空间添加成员并分配角色。"""
|
|
191
|
+
raise NotImplementedError(
|
|
192
|
+
f"{_V6_PREFIX}:请通过 get_v6_port(MULTI_AGENT_COLLABORATION, manager)"
|
|
193
|
+
f" 或 MindForge.multi_agent 使用正式实现。"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# —— 记忆共享 ——
|
|
197
|
+
|
|
198
|
+
def share_memory(self, space_ref: str, memory_id: str,
|
|
199
|
+
actor: str) -> Dict[str, Any]:
|
|
200
|
+
"""把记忆共享进空间。"""
|
|
201
|
+
raise NotImplementedError(
|
|
202
|
+
f"{_V6_PREFIX}:请通过 get_v6_port(MULTI_AGENT_COLLABORATION, manager)"
|
|
203
|
+
f" 或 MindForge.multi_agent 使用正式实现。"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def list_space_memories(self, space_ref: str, actor: str,
|
|
207
|
+
limit: int = 50, offset: int = 0) -> Dict[str, Any]:
|
|
208
|
+
"""列出空间中的记忆。"""
|
|
209
|
+
raise NotImplementedError(
|
|
210
|
+
f"{_V6_PREFIX}:请通过 get_v6_port(MULTI_AGENT_COLLABORATION, manager)"
|
|
211
|
+
f" 或 MindForge.multi_agent 使用正式实现。"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# —— 规划中的高级能力(当前版本未提供)——
|
|
215
|
+
|
|
216
|
+
def orchestrate_task(self, task: Dict[str, Any]) -> Dict[str, Any]:
|
|
217
|
+
"""任务编排:拆分 → 派发 → 汇总 → 回溯(规划中,尚未实现)。"""
|
|
218
|
+
raise NotImplementedError(
|
|
219
|
+
f"{_V6_PREFIX} 规划中:{self.capability.value}.orchestrate_task"
|
|
220
|
+
f" 尚未实现。"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def trace_memory_lineage(self, memory_id: str) -> Dict[str, Any]:
|
|
224
|
+
"""跨 Agent 记忆溯源与贡献度归因(规划中,尚未实现)。"""
|
|
225
|
+
raise NotImplementedError(
|
|
226
|
+
f"{_V6_PREFIX} 规划中:{self.capability.value}.trace_memory_lineage"
|
|
227
|
+
f" 尚未实现。"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class MultiAgentAdapterPort(MultiAgentCollaborationPort):
|
|
232
|
+
"""多 Agent 适配器端口(正式)
|
|
233
|
+
|
|
234
|
+
把 `MultiAgentMemoryManager` 挂到 v6 端口上,**不重写其逻辑**:
|
|
235
|
+
已实现的方法直接转发,规划中的方法沿用基类(抛 NotImplementedError)。
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
STATUS = V6Status.AVAILABLE
|
|
239
|
+
requires_args = True
|
|
240
|
+
|
|
241
|
+
def __init__(self, manager: Any) -> None:
|
|
242
|
+
"""用既有的多 Agent 管理器实例初始化。
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
manager: `modules.multi_agent.MultiAgentMemoryManager` 实例
|
|
246
|
+
"""
|
|
247
|
+
self._manager = manager
|
|
248
|
+
|
|
249
|
+
def status(self) -> V6Status:
|
|
250
|
+
"""正式发布,返回 AVAILABLE。"""
|
|
251
|
+
return self.STATUS
|
|
252
|
+
|
|
253
|
+
def create_space(self, name: str, owner_agent: str,
|
|
254
|
+
description: str = "", policy: str = "shared") -> Dict[str, Any]:
|
|
255
|
+
"""转发到 `MultiAgentMemoryManager.create_space()`。"""
|
|
256
|
+
return self._manager.create_space(
|
|
257
|
+
name=name, owner_agent=owner_agent,
|
|
258
|
+
description=description, policy=policy,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
def list_spaces(self, agent_id: str = "") -> List[Dict[str, Any]]:
|
|
262
|
+
"""转发到 `MultiAgentMemoryManager.list_spaces()`。"""
|
|
263
|
+
return self._manager.list_spaces(agent_id=agent_id)
|
|
264
|
+
|
|
265
|
+
def add_member(self, space_ref: str, agent_id: str,
|
|
266
|
+
role: str = "reader", actor: str = "") -> Dict[str, Any]:
|
|
267
|
+
"""转发到 `MultiAgentMemoryManager.add_member()`。"""
|
|
268
|
+
return self._manager.add_member(
|
|
269
|
+
space_ref=space_ref, agent_id=agent_id, role=role, actor=actor,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def share_memory(self, space_ref: str, memory_id: str,
|
|
273
|
+
actor: str) -> Dict[str, Any]:
|
|
274
|
+
"""转发到 `MultiAgentMemoryManager.share_memory()`。"""
|
|
275
|
+
return self._manager.share_memory(
|
|
276
|
+
space_ref=space_ref, memory_id=memory_id, actor=actor,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
def list_space_memories(self, space_ref: str, actor: str,
|
|
280
|
+
limit: int = 50, offset: int = 0) -> Dict[str, Any]:
|
|
281
|
+
"""转发到 `MultiAgentMemoryManager.list_space_memories()`。"""
|
|
282
|
+
return self._manager.list_space_memories(
|
|
283
|
+
space_ref=space_ref, actor=actor, limit=limit, offset=offset,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def summary(self, space_ref: str = "") -> Dict[str, Any]:
|
|
287
|
+
"""返回既有管理器的统计概览(转发 `space_stats()`)。"""
|
|
288
|
+
return self._manager.space_stats(space_ref=space_ref)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def create_multi_agent_adapter(manager: Any) -> MultiAgentAdapterPort:
|
|
292
|
+
"""工厂函数:把既有多 Agent 管理器包装成 v6 端口实例。
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
manager: `modules.multi_agent.MultiAgentMemoryManager` 实例
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
已绑定管理器的 `MultiAgentAdapterPort` 实例
|
|
299
|
+
"""
|
|
300
|
+
return MultiAgentAdapterPort(manager)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# ---------------------------------------------------------------------------
|
|
304
|
+
# 端口 2:6G AI 记忆预览(正式发布)
|
|
305
|
+
# ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
class MemoryPreviewPort(V6Port):
|
|
308
|
+
"""6G AI 记忆预览端口(正式实现)
|
|
309
|
+
|
|
310
|
+
与稳定版并行的实验分支:Preview Channel 使用**独立的 SQLite 预览库**
|
|
311
|
+
(互不污染主库),支持上下文蒸馏、增量索引统计与单记忆时间线。
|
|
312
|
+
|
|
313
|
+
构造需要 `storage`(主库 `MindForgeStorage` 实例)。
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
capability = V6Capability.MEMORY_PREVIEW
|
|
317
|
+
STATUS = V6Status.AVAILABLE
|
|
318
|
+
requires_args = True
|
|
319
|
+
|
|
320
|
+
def __init__(self, storage: Any) -> None:
|
|
321
|
+
"""用主库存储实例初始化。
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
storage: `mindforge.core.storage.MindForgeStorage` 实例
|
|
325
|
+
"""
|
|
326
|
+
self.storage = storage
|
|
327
|
+
self._data_dir = getattr(storage, "data_dir", None) or "."
|
|
328
|
+
self._channels_dir = os.path.join(self._data_dir, _PREVIEW_DIRNAME)
|
|
329
|
+
os.makedirs(self._channels_dir, exist_ok=True)
|
|
330
|
+
self._registry_path = os.path.join(self._channels_dir, "channels.db")
|
|
331
|
+
|
|
332
|
+
def status(self) -> V6Status:
|
|
333
|
+
"""6G AI 记忆预览已随 v6.0.0 正式发布,返回 AVAILABLE。"""
|
|
334
|
+
return self.STATUS
|
|
335
|
+
|
|
336
|
+
# ===== 通道管理 =====
|
|
337
|
+
|
|
338
|
+
def _registry_conn(self) -> sqlite3.Connection:
|
|
339
|
+
conn = sqlite3.connect(self._registry_path)
|
|
340
|
+
conn.row_factory = sqlite3.Row
|
|
341
|
+
conn.execute("""
|
|
342
|
+
CREATE TABLE IF NOT EXISTS preview_channels (
|
|
343
|
+
channel_id TEXT PRIMARY KEY,
|
|
344
|
+
name TEXT DEFAULT '',
|
|
345
|
+
db_path TEXT NOT NULL,
|
|
346
|
+
status TEXT DEFAULT 'open',
|
|
347
|
+
config TEXT DEFAULT '{}',
|
|
348
|
+
created_at REAL,
|
|
349
|
+
updated_at REAL
|
|
350
|
+
)
|
|
351
|
+
""")
|
|
352
|
+
conn.commit()
|
|
353
|
+
return conn
|
|
354
|
+
|
|
355
|
+
def open_preview_channel(self,
|
|
356
|
+
config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
357
|
+
"""开启预览通道(与稳定版并行、互不污染)。
|
|
358
|
+
|
|
359
|
+
每个通道是一个独立的 SQLite 库文件,通道内的记忆快照/实验数据
|
|
360
|
+
不会写入主库;关闭通道后可一键清理。
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
config: 可选配置,如 {"name": "实验分支", "note": "..."}
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
{
|
|
367
|
+
"success": True, "channel_id": ..., "name": ...,
|
|
368
|
+
"db_path": ..., "status": "open", "created_at": ...
|
|
369
|
+
}
|
|
370
|
+
"""
|
|
371
|
+
config = dict(config or {})
|
|
372
|
+
name = str(config.pop("name", ""))[:64] or f"preview_{time.strftime('%Y%m%d_%H%M%S')}"
|
|
373
|
+
channel_id = f"pv_{uuid.uuid4().hex[:12]}"
|
|
374
|
+
db_path = os.path.join(self._channels_dir, f"{channel_id}.db")
|
|
375
|
+
now = time.time()
|
|
376
|
+
|
|
377
|
+
conn = sqlite3.connect(db_path)
|
|
378
|
+
conn.execute("""
|
|
379
|
+
CREATE TABLE IF NOT EXISTS channel_memories (
|
|
380
|
+
id TEXT PRIMARY KEY,
|
|
381
|
+
content TEXT NOT NULL,
|
|
382
|
+
category TEXT DEFAULT 'general',
|
|
383
|
+
tags TEXT DEFAULT '[]',
|
|
384
|
+
importance TEXT DEFAULT 'MEDIUM',
|
|
385
|
+
source_memory_id TEXT DEFAULT '',
|
|
386
|
+
created_at REAL
|
|
387
|
+
)
|
|
388
|
+
""")
|
|
389
|
+
conn.commit()
|
|
390
|
+
conn.close()
|
|
391
|
+
|
|
392
|
+
reg = self._registry_conn()
|
|
393
|
+
reg.execute(
|
|
394
|
+
"INSERT INTO preview_channels (channel_id, name, db_path, status,"
|
|
395
|
+
" config, created_at, updated_at) VALUES (?, ?, ?, 'open', ?, ?, ?)",
|
|
396
|
+
(channel_id, name, db_path, json.dumps(config), now, now),
|
|
397
|
+
)
|
|
398
|
+
reg.commit()
|
|
399
|
+
reg.close()
|
|
400
|
+
return {
|
|
401
|
+
"success": True,
|
|
402
|
+
"channel_id": channel_id,
|
|
403
|
+
"name": name,
|
|
404
|
+
"db_path": db_path,
|
|
405
|
+
"status": "open",
|
|
406
|
+
"config": config,
|
|
407
|
+
"created_at": now,
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
def close_preview_channel(self, channel_id: str,
|
|
411
|
+
purge: bool = True) -> Dict[str, Any]:
|
|
412
|
+
"""关闭并清理预览通道。
|
|
413
|
+
|
|
414
|
+
将通道状态置为 closed;`purge=True`(默认)同时删除独立库文件,
|
|
415
|
+
`purge=False` 只标记关闭、保留文件供事后查看或重开。
|
|
416
|
+
|
|
417
|
+
v6.1.2 修复:`purge` 参数自 v6.0.0 起就写在本文档里(「可逆:仅标记时用
|
|
418
|
+
purge=False 保留文件」),但签名根本没有这个参数,函数体无条件删文件——
|
|
419
|
+
文档承诺的「保留文件」这一用法一直无法使用。现在参数真实存在并生效。
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
channel_id: 预览通道 ID
|
|
423
|
+
purge: 是否删除独立库文件(默认删除,与修复前行为一致)
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
关闭结果字典(新增 `purged` 字段标明文件是否已清理)
|
|
427
|
+
"""
|
|
428
|
+
reg = self._registry_conn()
|
|
429
|
+
row = reg.execute(
|
|
430
|
+
"SELECT * FROM preview_channels WHERE channel_id = ?",
|
|
431
|
+
(channel_id,)).fetchone()
|
|
432
|
+
if row is None:
|
|
433
|
+
reg.close()
|
|
434
|
+
return {"success": False, "error": f"预览通道不存在: {channel_id}"}
|
|
435
|
+
db_path = row["db_path"]
|
|
436
|
+
now = time.time()
|
|
437
|
+
reg.execute(
|
|
438
|
+
"UPDATE preview_channels SET status = 'closed', updated_at = ?"
|
|
439
|
+
" WHERE channel_id = ?", (now, channel_id))
|
|
440
|
+
reg.commit()
|
|
441
|
+
reg.close()
|
|
442
|
+
# 删除独立库文件(仅当存在且处于本通道目录内,防止路径穿越)
|
|
443
|
+
purged = False
|
|
444
|
+
if purge and db_path and os.path.abspath(db_path).startswith(
|
|
445
|
+
os.path.abspath(self._channels_dir)):
|
|
446
|
+
try:
|
|
447
|
+
if os.path.exists(db_path):
|
|
448
|
+
os.remove(db_path)
|
|
449
|
+
purged = True
|
|
450
|
+
except OSError:
|
|
451
|
+
pass
|
|
452
|
+
return {"success": True, "channel_id": channel_id,
|
|
453
|
+
"status": "closed", "closed_at": now,
|
|
454
|
+
"purged": purged, "db_path_kept": ("" if purged else db_path)}
|
|
455
|
+
|
|
456
|
+
def list_preview_channels(self, include_closed: bool = False) -> List[Dict[str, Any]]:
|
|
457
|
+
"""列出预览通道。"""
|
|
458
|
+
reg = self._registry_conn()
|
|
459
|
+
if include_closed:
|
|
460
|
+
rows = reg.execute(
|
|
461
|
+
"SELECT * FROM preview_channels ORDER BY created_at DESC").fetchall()
|
|
462
|
+
else:
|
|
463
|
+
rows = reg.execute(
|
|
464
|
+
"SELECT * FROM preview_channels WHERE status = 'open'"
|
|
465
|
+
" ORDER BY created_at DESC").fetchall()
|
|
466
|
+
reg.close()
|
|
467
|
+
result = []
|
|
468
|
+
for r in rows:
|
|
469
|
+
cfg = r["config"]
|
|
470
|
+
try:
|
|
471
|
+
cfg = json.loads(cfg) if cfg else {}
|
|
472
|
+
except (ValueError, TypeError):
|
|
473
|
+
cfg = {}
|
|
474
|
+
result.append({
|
|
475
|
+
"channel_id": r["channel_id"],
|
|
476
|
+
"name": r["name"],
|
|
477
|
+
"status": r["status"],
|
|
478
|
+
"created_at": r["created_at"],
|
|
479
|
+
"updated_at": r["updated_at"],
|
|
480
|
+
"config": cfg,
|
|
481
|
+
})
|
|
482
|
+
return result
|
|
483
|
+
|
|
484
|
+
def channel_stats(self, channel_id: str) -> Dict[str, Any]:
|
|
485
|
+
"""预览通道统计(记忆快照数、分类分布)。"""
|
|
486
|
+
reg = self._registry_conn()
|
|
487
|
+
row = reg.execute(
|
|
488
|
+
"SELECT * FROM preview_channels WHERE channel_id = ?",
|
|
489
|
+
(channel_id,)).fetchone()
|
|
490
|
+
reg.close()
|
|
491
|
+
if row is None:
|
|
492
|
+
return {"success": False, "error": f"预览通道不存在: {channel_id}"}
|
|
493
|
+
if not os.path.exists(row["db_path"]):
|
|
494
|
+
return {"success": False, "error": "预览库文件不存在(可能已关闭清理)"}
|
|
495
|
+
conn = sqlite3.connect(row["db_path"])
|
|
496
|
+
conn.row_factory = sqlite3.Row
|
|
497
|
+
total = conn.execute(
|
|
498
|
+
"SELECT COUNT(*) AS c FROM channel_memories").fetchone()["c"]
|
|
499
|
+
by_cat = conn.execute(
|
|
500
|
+
"SELECT category, COUNT(*) AS c FROM channel_memories"
|
|
501
|
+
" GROUP BY category").fetchall()
|
|
502
|
+
conn.close()
|
|
503
|
+
return {
|
|
504
|
+
"success": True,
|
|
505
|
+
"channel_id": channel_id,
|
|
506
|
+
"name": row["name"],
|
|
507
|
+
"status": row["status"],
|
|
508
|
+
"total_memories": total,
|
|
509
|
+
"by_category": {r["category"]: r["c"] for r in by_cat},
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
def snapshot_memory(self, channel_id: str, memory_id: str) -> Dict[str, Any]:
|
|
513
|
+
"""把主库的一条记忆快照进预览通道(不修改主库)。"""
|
|
514
|
+
entry = self.storage.get_memory(memory_id)
|
|
515
|
+
if entry is None:
|
|
516
|
+
return {"success": False, "error": "记忆不存在"}
|
|
517
|
+
reg = self._registry_conn()
|
|
518
|
+
row = reg.execute(
|
|
519
|
+
"SELECT * FROM preview_channels WHERE channel_id = ?",
|
|
520
|
+
(channel_id,)).fetchone()
|
|
521
|
+
reg.close()
|
|
522
|
+
if row is None:
|
|
523
|
+
return {"success": False, "error": f"预览通道不存在: {channel_id}"}
|
|
524
|
+
if not os.path.exists(row["db_path"]):
|
|
525
|
+
return {"success": False, "error": "预览库文件不存在(可能已关闭清理)"}
|
|
526
|
+
conn = sqlite3.connect(row["db_path"])
|
|
527
|
+
# v6.1.3 修复:tags 需按 JSON 序列化。此前用 str(list) 存的是
|
|
528
|
+
# Python repr("['a', 'b']"),下游 json.loads 解析必然失败,
|
|
529
|
+
# 与主库 tags TEXT DEFAULT '[]' 的 JSON 约定不一致。
|
|
530
|
+
conn.execute(
|
|
531
|
+
"INSERT OR REPLACE INTO channel_memories (id, content, category,"
|
|
532
|
+
" tags, importance, source_memory_id, created_at)"
|
|
533
|
+
" VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
534
|
+
(memory_id, entry.content or "", entry.category,
|
|
535
|
+
json.dumps(entry.tags or [], ensure_ascii=False),
|
|
536
|
+
entry.importance.value, memory_id, time.time()),
|
|
537
|
+
)
|
|
538
|
+
conn.commit()
|
|
539
|
+
conn.close()
|
|
540
|
+
return {"success": True, "channel_id": channel_id,
|
|
541
|
+
"memory_id": memory_id, "snapshotted": True}
|
|
542
|
+
|
|
543
|
+
# ===== 上下文蒸馏 =====
|
|
544
|
+
|
|
545
|
+
def distill_context(self, memory_ids: List[str],
|
|
546
|
+
max_tokens: int = 2000) -> Dict[str, Any]:
|
|
547
|
+
"""上下文蒸馏:长程记忆分层摘要与 Token 压缩。
|
|
548
|
+
|
|
549
|
+
按重要度分层压缩:
|
|
550
|
+
- HIGH :保留全文(高价值记忆不舍弃)
|
|
551
|
+
- MEDIUM :截断至预算配额内的摘要
|
|
552
|
+
- LOW :仅保留标签与首句
|
|
553
|
+
|
|
554
|
+
在总预算 `max_tokens` 内按重要度配额分配:HIGH 40% / MEDIUM 40% /
|
|
555
|
+
LOW 20%;配额不足时优先保证 HIGH。
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
memory_ids: 要蒸馏的记忆 ID 列表
|
|
559
|
+
max_tokens: Token 预算上限(默认 2000)
|
|
560
|
+
|
|
561
|
+
Returns:
|
|
562
|
+
{
|
|
563
|
+
"success": True,
|
|
564
|
+
"layers": {"HIGH": [...], "MEDIUM": [...], "LOW": [...]},
|
|
565
|
+
"total_original_tokens": ...,
|
|
566
|
+
"total_distilled_tokens": ...,
|
|
567
|
+
"compression_ratio": 0.0~1.0,
|
|
568
|
+
"budget": max_tokens,
|
|
569
|
+
"omitted_count": 被预算裁掉的记忆条数,
|
|
570
|
+
}
|
|
571
|
+
"""
|
|
572
|
+
if not isinstance(memory_ids, list):
|
|
573
|
+
return {"success": False, "error": "memory_ids 必须是列表"}
|
|
574
|
+
if max_tokens < 1:
|
|
575
|
+
max_tokens = 1
|
|
576
|
+
layers: Dict[str, List[Dict[str, Any]]] = {
|
|
577
|
+
"HIGH": [], "MEDIUM": [], "LOW": [],
|
|
578
|
+
}
|
|
579
|
+
total_original = 0
|
|
580
|
+
fetched = []
|
|
581
|
+
for mid in memory_ids:
|
|
582
|
+
# v6.1.2:列表里混进非字符串元素(例如直接把 add() 返回的 MemoryEntry
|
|
583
|
+
# 对象整批传进来)时,原来会让 get_memory 抛 ValueError 而整次蒸馏失败。
|
|
584
|
+
# 现按 core/types.coerce_memory_id 归一:对象取 .id,识别不了的跳过。
|
|
585
|
+
mid = coerce_memory_id(mid)
|
|
586
|
+
if mid is None:
|
|
587
|
+
continue
|
|
588
|
+
entry = self.storage.get_memory(mid)
|
|
589
|
+
if entry is None:
|
|
590
|
+
continue
|
|
591
|
+
content = entry.content or ""
|
|
592
|
+
total_original += _estimate_tokens(content)
|
|
593
|
+
importance = getattr(entry.importance, "value", None) or str(
|
|
594
|
+
entry.importance).upper()
|
|
595
|
+
fetched.append((importance, mid, content, entry))
|
|
596
|
+
|
|
597
|
+
# 预算配额:HIGH 40% / MEDIUM 40% / LOW 20%
|
|
598
|
+
high_budget = int(max_tokens * 0.4)
|
|
599
|
+
med_budget = int(max_tokens * 0.4)
|
|
600
|
+
low_budget = max(0, max_tokens - high_budget - med_budget)
|
|
601
|
+
budgets = {"HIGH": high_budget, "MEDIUM": med_budget, "LOW": low_budget}
|
|
602
|
+
spent = {"HIGH": 0, "MEDIUM": 0, "LOW": 0}
|
|
603
|
+
omitted = 0
|
|
604
|
+
|
|
605
|
+
for importance, mid, content, entry in fetched:
|
|
606
|
+
key = importance if importance in budgets else "MEDIUM"
|
|
607
|
+
budget = budgets[key]
|
|
608
|
+
used = spent[key]
|
|
609
|
+
if used >= budget:
|
|
610
|
+
omitted += 1
|
|
611
|
+
continue
|
|
612
|
+
# 按层处理
|
|
613
|
+
if key == "HIGH":
|
|
614
|
+
text = content # 高价值记忆保留全文
|
|
615
|
+
elif key == "MEDIUM":
|
|
616
|
+
# v6.1.2 修复:本行注释自 v6.0.0 起就承诺「前 60% + 末尾 20%(保头尾
|
|
617
|
+
# 信息)」,代码却只写了 `content[:cap * 2]`——只有头部,结尾全丢,
|
|
618
|
+
# 而 `cap * 2`(token 估算值 ×2 当字符数用)也让实际保留比例漂移。
|
|
619
|
+
# 现在按字符占比真正保头保尾,中段用省略号标明被压缩。
|
|
620
|
+
length = len(content)
|
|
621
|
+
head = int(length * 0.6)
|
|
622
|
+
tail = int(length * 0.2)
|
|
623
|
+
if length <= 40 or head + tail >= length:
|
|
624
|
+
text = content # 本就不长,无需压缩
|
|
625
|
+
else:
|
|
626
|
+
text = (content[:head] + " …[中段已压缩]… "
|
|
627
|
+
+ content[length - tail:])
|
|
628
|
+
else: # LOW:仅标签 + 首句
|
|
629
|
+
text = content.split("\n")[0][:120] if content else ""
|
|
630
|
+
tok = _estimate_tokens(text)
|
|
631
|
+
if used + tok > budget:
|
|
632
|
+
# 截断到剩余配额
|
|
633
|
+
remain_chars = int((budget - used) * 2)
|
|
634
|
+
text = text[:max(0, remain_chars)]
|
|
635
|
+
tok = _estimate_tokens(text)
|
|
636
|
+
if tok <= 0:
|
|
637
|
+
omitted += 1
|
|
638
|
+
continue
|
|
639
|
+
spent[key] += tok
|
|
640
|
+
layers[key].append({
|
|
641
|
+
"memory_id": mid,
|
|
642
|
+
"category": entry.category,
|
|
643
|
+
"tags": entry.tags,
|
|
644
|
+
"importance": key,
|
|
645
|
+
"content": text,
|
|
646
|
+
"tokens": tok,
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
total_distilled = sum(spent.values())
|
|
650
|
+
return {
|
|
651
|
+
"success": True,
|
|
652
|
+
"layers": layers,
|
|
653
|
+
"total_original_tokens": total_original,
|
|
654
|
+
"total_distilled_tokens": total_distilled,
|
|
655
|
+
"compression_ratio": round(
|
|
656
|
+
(1 - total_distilled / total_original), 4)
|
|
657
|
+
if total_original > 0 else 0.0,
|
|
658
|
+
"budget": max_tokens,
|
|
659
|
+
"omitted_count": omitted,
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
# ===== 增量索引统计 =====
|
|
663
|
+
|
|
664
|
+
def incremental_index_stats(self) -> Dict[str, Any]:
|
|
665
|
+
"""增量索引统计:写入即索引的近实时召回指标。
|
|
666
|
+
|
|
667
|
+
统计主库记忆总量、近 24h / 近 7d 增量、索引覆盖
|
|
668
|
+
(FTS 可用性、嵌入后端可用性)与最新写入延迟。
|
|
669
|
+
|
|
670
|
+
Returns:
|
|
671
|
+
{
|
|
672
|
+
"success": True,
|
|
673
|
+
"total_memories": ...,
|
|
674
|
+
"new_24h": ...,
|
|
675
|
+
"new_7d": ...,
|
|
676
|
+
"index": {"fts_available": bool, "embedding_available": bool},
|
|
677
|
+
"latest_write_lag_seconds": ...,
|
|
678
|
+
"generated_at": ...,
|
|
679
|
+
}
|
|
680
|
+
"""
|
|
681
|
+
conn = self.storage._get_conn()
|
|
682
|
+
now = time.time()
|
|
683
|
+
day = 86400.0
|
|
684
|
+
total = conn.execute(
|
|
685
|
+
"SELECT COUNT(*) AS c FROM memories WHERE category != 'trash'"
|
|
686
|
+
).fetchone()["c"]
|
|
687
|
+
new_24h = conn.execute(
|
|
688
|
+
"SELECT COUNT(*) AS c FROM memories WHERE created_at >= ?"
|
|
689
|
+
" AND category != 'trash'", (now - day,)).fetchone()["c"]
|
|
690
|
+
new_7d = conn.execute(
|
|
691
|
+
"SELECT COUNT(*) AS c FROM memories WHERE created_at >= ?"
|
|
692
|
+
" AND category != 'trash'", (now - 7 * day,)).fetchone()["c"]
|
|
693
|
+
latest = conn.execute(
|
|
694
|
+
"SELECT MAX(created_at) AS m FROM memories WHERE category != 'trash'"
|
|
695
|
+
).fetchone()["m"]
|
|
696
|
+
lag = max(0.0, now - (latest or now))
|
|
697
|
+
fts_available = False
|
|
698
|
+
try:
|
|
699
|
+
fts_row = conn.execute(
|
|
700
|
+
"SELECT 1 FROM sqlite_master WHERE type = 'table'"
|
|
701
|
+
" AND name = 'memory_fts'").fetchone()
|
|
702
|
+
fts_available = fts_row is not None
|
|
703
|
+
except sqlite3.Error:
|
|
704
|
+
fts_available = False
|
|
705
|
+
embedding_available = False
|
|
706
|
+
try:
|
|
707
|
+
# v6.1.2 修复:原来调 `self.storage.get_embedding_status()`,但那是
|
|
708
|
+
# **门面**(core/mindforge.py:759)上的方法,StorageEngine 并没有它;
|
|
709
|
+
# AttributeError 又被宽 except 吞掉,于是 `embedding_available` 恒为
|
|
710
|
+
# False——「嵌入后端就绪」这一项在健康报告里永远显示不可用,装好了
|
|
711
|
+
# 后端也一样。改读存储上的 embedding_engine 属性(门面内部也是读它)。
|
|
712
|
+
engine = getattr(self.storage, "embedding_engine", None)
|
|
713
|
+
embedding_available = bool(engine is not None
|
|
714
|
+
and getattr(engine, "is_available", False))
|
|
715
|
+
except Exception:
|
|
716
|
+
embedding_available = False
|
|
717
|
+
return {
|
|
718
|
+
"success": True,
|
|
719
|
+
"total_memories": total,
|
|
720
|
+
"new_24h": new_24h,
|
|
721
|
+
"new_7d": new_7d,
|
|
722
|
+
"index": {
|
|
723
|
+
"fts_available": fts_available,
|
|
724
|
+
"embedding_available": embedding_available,
|
|
725
|
+
},
|
|
726
|
+
"latest_write_lag_seconds": round(lag, 1),
|
|
727
|
+
"generated_at": now,
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
# ===== 单记忆时间线 =====
|
|
731
|
+
|
|
732
|
+
def timeline(self, memory_id: str) -> Dict[str, Any]:
|
|
733
|
+
"""记忆时间线可视化数据(单记忆:版本历史 + 演变历史 + 访问足迹)。
|
|
734
|
+
|
|
735
|
+
Args:
|
|
736
|
+
memory_id: 记忆 ID
|
|
737
|
+
|
|
738
|
+
Returns:
|
|
739
|
+
{
|
|
740
|
+
"success": True,
|
|
741
|
+
"memory_id": ...,
|
|
742
|
+
"current": {...当前记忆摘要...},
|
|
743
|
+
"versions": [...版本历史...],
|
|
744
|
+
"evolutions": [...演变记录...],
|
|
745
|
+
"audit_trail": [...审计足迹...],
|
|
746
|
+
}
|
|
747
|
+
"""
|
|
748
|
+
entry = self.storage.get_memory(memory_id)
|
|
749
|
+
versions = self.storage.list_versions(memory_id, limit=50)
|
|
750
|
+
evolutions = self.storage.list_evolutions(memory_id, limit=50)
|
|
751
|
+
audit = []
|
|
752
|
+
try:
|
|
753
|
+
conn = self.storage._get_conn()
|
|
754
|
+
rows = conn.execute(
|
|
755
|
+
"SELECT action, actor, timestamp, privacy_level FROM audit_log"
|
|
756
|
+
" WHERE memory_id = ? ORDER BY timestamp DESC LIMIT 20",
|
|
757
|
+
(memory_id,)).fetchall()
|
|
758
|
+
audit = [{
|
|
759
|
+
"action": r["action"],
|
|
760
|
+
"actor": r["actor"],
|
|
761
|
+
"at": r["timestamp"],
|
|
762
|
+
"privacy": r["privacy_level"],
|
|
763
|
+
} for r in rows]
|
|
764
|
+
except sqlite3.Error:
|
|
765
|
+
audit = []
|
|
766
|
+
current = None
|
|
767
|
+
if entry is not None:
|
|
768
|
+
current = {
|
|
769
|
+
"memory_id": entry.id,
|
|
770
|
+
"content_preview": (entry.content or "")[:120],
|
|
771
|
+
"category": entry.category,
|
|
772
|
+
"tags": entry.tags,
|
|
773
|
+
"importance": entry.importance.value,
|
|
774
|
+
"created_at": entry.created_at,
|
|
775
|
+
"updated_at": entry.updated_at,
|
|
776
|
+
}
|
|
777
|
+
return {
|
|
778
|
+
"success": True,
|
|
779
|
+
"memory_id": memory_id,
|
|
780
|
+
"current": current,
|
|
781
|
+
"versions": versions,
|
|
782
|
+
"evolutions": evolutions,
|
|
783
|
+
"audit_trail": audit,
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
def summary(self) -> Dict[str, Any]:
|
|
787
|
+
"""6G 记忆预览端口概览。"""
|
|
788
|
+
stats = self.incremental_index_stats()
|
|
789
|
+
channels = self.list_preview_channels()
|
|
790
|
+
return {
|
|
791
|
+
"channels_open": len(channels),
|
|
792
|
+
"channels": channels,
|
|
793
|
+
"index": stats.get("index", {}),
|
|
794
|
+
"total_memories": stats.get("total_memories", 0),
|
|
795
|
+
"new_24h": stats.get("new_24h", 0),
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
# ---------------------------------------------------------------------------
|
|
800
|
+
# 端口注册表
|
|
801
|
+
# ---------------------------------------------------------------------------
|
|
802
|
+
|
|
803
|
+
#: 能力值 → 端口类 的注册表
|
|
804
|
+
_V6_PORT_REGISTRY: Dict[str, Type[V6Port]] = {}
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def register_v6_port(port_cls: Type[V6Port],
|
|
808
|
+
override: bool = False) -> Type[V6Port]:
|
|
809
|
+
"""注册(或替换)一个 v6 端口实现。
|
|
810
|
+
|
|
811
|
+
Args:
|
|
812
|
+
port_cls: `V6Port` 的子类,必须声明 `capability`
|
|
813
|
+
override: 同名能力已注册时是否覆盖,默认 False
|
|
814
|
+
|
|
815
|
+
Returns:
|
|
816
|
+
传入的 port_cls,便于作为装饰器使用
|
|
817
|
+
|
|
818
|
+
Raises:
|
|
819
|
+
TypeError: port_cls 不是 V6Port 的子类
|
|
820
|
+
ValueError: capability 非法,或已注册且未允许覆盖
|
|
821
|
+
"""
|
|
822
|
+
if not isinstance(port_cls, type) or not issubclass(port_cls, V6Port):
|
|
823
|
+
raise TypeError(f"port_cls 必须是 V6Port 的子类,收到: {port_cls!r}")
|
|
824
|
+
capability = getattr(port_cls, "capability", None)
|
|
825
|
+
if not isinstance(capability, V6Capability):
|
|
826
|
+
raise ValueError(f"port_cls 必须声明合法的 capability,收到: {capability!r}")
|
|
827
|
+
key = capability.value
|
|
828
|
+
if key in _V6_PORT_REGISTRY and not override:
|
|
829
|
+
raise ValueError(
|
|
830
|
+
f"能力 {key} 已注册端口 {_V6_PORT_REGISTRY[key].__name__};"
|
|
831
|
+
f"如需替换请传 override=True"
|
|
832
|
+
)
|
|
833
|
+
_V6_PORT_REGISTRY[key] = port_cls
|
|
834
|
+
return port_cls
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
def unregister_v6_port(capability: V6Capability) -> bool:
|
|
838
|
+
"""注销指定能力的端口。
|
|
839
|
+
|
|
840
|
+
Args:
|
|
841
|
+
capability: 要注销的 v6 能力
|
|
842
|
+
|
|
843
|
+
Returns:
|
|
844
|
+
是否确实移除了一条已注册的端口
|
|
845
|
+
"""
|
|
846
|
+
return _V6_PORT_REGISTRY.pop(capability.value, None) is not None
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def get_v6_port(capability: V6Capability, *args: Any,
|
|
850
|
+
**kwargs: Any) -> Optional[V6Port]:
|
|
851
|
+
"""获取指定能力当前注册的端口实例。
|
|
852
|
+
|
|
853
|
+
Args:
|
|
854
|
+
capability: v6 能力枚举
|
|
855
|
+
*args: 传给端口构造器的位置参数(如 MemoryPreviewPort 需要 storage)
|
|
856
|
+
**kwargs: 传给端口构造器的关键字参数
|
|
857
|
+
|
|
858
|
+
Returns:
|
|
859
|
+
端口实例;该能力未注册时返回 None
|
|
860
|
+
|
|
861
|
+
Raises:
|
|
862
|
+
TypeError: 构造器参数不匹配(例如 MemoryPreviewPort 缺少 storage)
|
|
863
|
+
"""
|
|
864
|
+
port_cls = _V6_PORT_REGISTRY.get(capability.value)
|
|
865
|
+
if port_cls is None:
|
|
866
|
+
return None
|
|
867
|
+
return port_cls(*args, **kwargs)
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
def list_v6_ports() -> List[Dict[str, Any]]:
|
|
871
|
+
"""列出所有已注册端口的状态(不实例化需要构造参数的端口)。
|
|
872
|
+
|
|
873
|
+
Returns:
|
|
874
|
+
每个元素为端口描述字典,含 capability / status / available /
|
|
875
|
+
port / target_version / requires_args。
|
|
876
|
+
"""
|
|
877
|
+
result: List[Dict[str, Any]] = []
|
|
878
|
+
for capability in V6Capability:
|
|
879
|
+
port_cls = _V6_PORT_REGISTRY.get(capability.value)
|
|
880
|
+
if port_cls is None:
|
|
881
|
+
continue
|
|
882
|
+
status = getattr(port_cls, "STATUS", V6Status.PLANNED)
|
|
883
|
+
result.append({
|
|
884
|
+
"capability": capability.value,
|
|
885
|
+
"status": status.value,
|
|
886
|
+
"available": status is V6Status.AVAILABLE,
|
|
887
|
+
"port": port_cls.__name__,
|
|
888
|
+
"target_version": V6_TARGET_VERSION,
|
|
889
|
+
"requires_args": bool(getattr(port_cls, "requires_args", False)),
|
|
890
|
+
})
|
|
891
|
+
return result
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
def v6_status() -> Dict[str, Any]:
|
|
895
|
+
"""返回 v6 端口总览(供 CLI / MCP / 官网调用)。
|
|
896
|
+
|
|
897
|
+
Returns:
|
|
898
|
+
{
|
|
899
|
+
"current_version": 当前版本号,
|
|
900
|
+
"target_version": "6.0.0",
|
|
901
|
+
"released": v6 是否已发布,
|
|
902
|
+
"ports": [ ... 各端口描述 ... ],
|
|
903
|
+
}
|
|
904
|
+
"""
|
|
905
|
+
return {
|
|
906
|
+
"current_version": __version__,
|
|
907
|
+
"target_version": V6_TARGET_VERSION,
|
|
908
|
+
"released": True,
|
|
909
|
+
"ports": list_v6_ports(),
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
# 默认注册:多 Agent 经适配器挂载(需 manager 参数)、6G 记忆预览需 storage 参数
|
|
914
|
+
register_v6_port(MultiAgentAdapterPort)
|
|
915
|
+
register_v6_port(MemoryPreviewPort)
|
|
916
|
+
|
|
917
|
+
__all__ = [
|
|
918
|
+
"V6_TARGET_VERSION",
|
|
919
|
+
"V6Status",
|
|
920
|
+
"V6Capability",
|
|
921
|
+
"V6Port",
|
|
922
|
+
"MultiAgentCollaborationPort",
|
|
923
|
+
"MultiAgentAdapterPort",
|
|
924
|
+
"MemoryPreviewPort",
|
|
925
|
+
"create_multi_agent_adapter",
|
|
926
|
+
"register_v6_port",
|
|
927
|
+
"unregister_v6_port",
|
|
928
|
+
"get_v6_port",
|
|
929
|
+
"list_v6_ports",
|
|
930
|
+
"v6_status",
|
|
931
|
+
]
|