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/core/types.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MindForge v5.0 类型定义
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import List, Optional, Dict, Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PrivacyLevel(Enum):
|
|
11
|
+
"""隐私级别"""
|
|
12
|
+
PUBLIC = "PUBLIC"
|
|
13
|
+
INTERNAL = "INTERNAL"
|
|
14
|
+
PRIVATE = "PRIVATE"
|
|
15
|
+
STRICT = "STRICT"
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def from_string(cls, value: str) -> "PrivacyLevel":
|
|
19
|
+
try:
|
|
20
|
+
return cls(value.upper())
|
|
21
|
+
except ValueError:
|
|
22
|
+
return cls.INTERNAL
|
|
23
|
+
|
|
24
|
+
def to_int(self) -> int:
|
|
25
|
+
mapping = {"PUBLIC": 0, "INTERNAL": 1, "PRIVATE": 2, "STRICT": 3}
|
|
26
|
+
return mapping[self.value]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Importance(Enum):
|
|
30
|
+
"""重要性级别"""
|
|
31
|
+
LOW = "LOW"
|
|
32
|
+
MEDIUM = "MEDIUM"
|
|
33
|
+
HIGH = "HIGH"
|
|
34
|
+
CRITICAL = "CRITICAL"
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_string(cls, value: str) -> "Importance":
|
|
38
|
+
try:
|
|
39
|
+
return cls(value.upper())
|
|
40
|
+
except ValueError:
|
|
41
|
+
return cls.MEDIUM
|
|
42
|
+
|
|
43
|
+
def to_int(self) -> int:
|
|
44
|
+
mapping = {"LOW": 0, "MEDIUM": 1, "HIGH": 2, "CRITICAL": 3}
|
|
45
|
+
return mapping[self.value]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class MemoryType(Enum):
|
|
49
|
+
"""记忆类型(多模态)"""
|
|
50
|
+
TEXT = "text"
|
|
51
|
+
IMAGE = "image"
|
|
52
|
+
AUDIO = "audio"
|
|
53
|
+
CODE = "code"
|
|
54
|
+
STRUCTURED = "structured"
|
|
55
|
+
MULTIMODAL = "multimodal"
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_string(cls, value: str) -> "MemoryType":
|
|
59
|
+
try:
|
|
60
|
+
return cls(value.lower())
|
|
61
|
+
except ValueError:
|
|
62
|
+
return cls.TEXT
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class MemoryLayer(Enum):
|
|
66
|
+
"""记忆层级(四层架构)"""
|
|
67
|
+
SENSORY = "sensory"
|
|
68
|
+
SHORT_TERM = "short_term"
|
|
69
|
+
LONG_TERM = "long_term"
|
|
70
|
+
PERMANENT = "permanent"
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def from_string(cls, value: str) -> "MemoryLayer":
|
|
74
|
+
try:
|
|
75
|
+
return cls(value.lower())
|
|
76
|
+
except ValueError:
|
|
77
|
+
return cls.SHORT_TERM
|
|
78
|
+
|
|
79
|
+
def to_int(self) -> int:
|
|
80
|
+
mapping = {"sensory": 0, "short_term": 1, "long_term": 2, "permanent": 3}
|
|
81
|
+
return mapping[self.value]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class MemoryConfig:
|
|
86
|
+
"""记忆系统配置"""
|
|
87
|
+
db_path: str = "./data/memory.db"
|
|
88
|
+
key_file: str = "./data/.key"
|
|
89
|
+
encrypted: bool = True
|
|
90
|
+
default_privacy: PrivacyLevel = PrivacyLevel.INTERNAL
|
|
91
|
+
default_importance: Importance = Importance.MEDIUM
|
|
92
|
+
default_layer: MemoryLayer = MemoryLayer.SHORT_TERM
|
|
93
|
+
auto_consolidate: bool = True
|
|
94
|
+
# v5.7.9:add/update 后自动抽取实体/关系入知识图谱(默认关闭,显式开启)
|
|
95
|
+
auto_extract_graph: bool = False
|
|
96
|
+
consolidate_interval_hours: int = 24
|
|
97
|
+
forget_curve_enabled: bool = True
|
|
98
|
+
vector_dim: int = 384
|
|
99
|
+
max_short_term_items: int = 100
|
|
100
|
+
sensory_buffer_size: int = 50
|
|
101
|
+
sensory_retention_seconds: int = 30
|
|
102
|
+
# v5.4.9 新增:Webhook / 事件通知配置
|
|
103
|
+
webhooks: Optional[List[Dict[str, Any]]] = None
|
|
104
|
+
webhook_secret: str = ""
|
|
105
|
+
event_bus_enabled: bool = True
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class DramaGenre(Enum):
|
|
109
|
+
"""短剧类型"""
|
|
110
|
+
ROMANCE = "romance"
|
|
111
|
+
SUSPENSE = "suspense"
|
|
112
|
+
COMEDY = "comedy"
|
|
113
|
+
ACTION = "action"
|
|
114
|
+
HORROR = "horror"
|
|
115
|
+
SCIFI = "scifi"
|
|
116
|
+
FANTASY = "fantasy"
|
|
117
|
+
DRAMA = "drama"
|
|
118
|
+
OTHER = "other"
|
|
119
|
+
|
|
120
|
+
@classmethod
|
|
121
|
+
def from_string(cls, value: str) -> "DramaGenre":
|
|
122
|
+
try:
|
|
123
|
+
return cls(value.lower())
|
|
124
|
+
except ValueError:
|
|
125
|
+
return cls.OTHER
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class DramaStatus(Enum):
|
|
129
|
+
"""短剧状态"""
|
|
130
|
+
WATCHING = "watching"
|
|
131
|
+
COMPLETED = "completed"
|
|
132
|
+
PLANNED = "planned"
|
|
133
|
+
DROPPED = "dropped"
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def from_string(cls, value: str) -> "DramaStatus":
|
|
137
|
+
try:
|
|
138
|
+
return cls(value.lower())
|
|
139
|
+
except ValueError:
|
|
140
|
+
return cls.PLANNED
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass
|
|
144
|
+
class DramaSeries:
|
|
145
|
+
"""短剧系列"""
|
|
146
|
+
id: str
|
|
147
|
+
title: str
|
|
148
|
+
genre: DramaGenre = DramaGenre.OTHER
|
|
149
|
+
total_episodes: int = 0
|
|
150
|
+
current_episode: int = 0
|
|
151
|
+
status: DramaStatus = DramaStatus.PLANNED
|
|
152
|
+
platform: str = ""
|
|
153
|
+
rating: float = 0.0
|
|
154
|
+
description: str = ""
|
|
155
|
+
tags: List[str] = field(default_factory=list)
|
|
156
|
+
cover_url: str = ""
|
|
157
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
158
|
+
created_at: float = 0.0
|
|
159
|
+
updated_at: float = 0.0
|
|
160
|
+
last_watched_at: float = 0.0
|
|
161
|
+
|
|
162
|
+
def to_dict(self) -> dict:
|
|
163
|
+
return {
|
|
164
|
+
"id": self.id,
|
|
165
|
+
"title": self.title,
|
|
166
|
+
"genre": self.genre.value,
|
|
167
|
+
"total_episodes": self.total_episodes,
|
|
168
|
+
"current_episode": self.current_episode,
|
|
169
|
+
"status": self.status.value,
|
|
170
|
+
"platform": self.platform,
|
|
171
|
+
"rating": self.rating,
|
|
172
|
+
"description": self.description,
|
|
173
|
+
"tags": self.tags,
|
|
174
|
+
"cover_url": self.cover_url,
|
|
175
|
+
"metadata": self.metadata,
|
|
176
|
+
"created_at": self.created_at,
|
|
177
|
+
"updated_at": self.updated_at,
|
|
178
|
+
"last_watched_at": self.last_watched_at,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass
|
|
183
|
+
class DramaScene:
|
|
184
|
+
"""短剧场次"""
|
|
185
|
+
id: str
|
|
186
|
+
drama_id: str
|
|
187
|
+
episode: int
|
|
188
|
+
scene_number: int
|
|
189
|
+
title: str
|
|
190
|
+
content: str = ""
|
|
191
|
+
location: str = ""
|
|
192
|
+
time_of_day: str = ""
|
|
193
|
+
tags: List[str] = field(default_factory=list)
|
|
194
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
195
|
+
created_at: float = 0.0
|
|
196
|
+
|
|
197
|
+
def to_dict(self) -> dict:
|
|
198
|
+
return {
|
|
199
|
+
"id": self.id,
|
|
200
|
+
"drama_id": self.drama_id,
|
|
201
|
+
"episode": self.episode,
|
|
202
|
+
"scene_number": self.scene_number,
|
|
203
|
+
"title": self.title,
|
|
204
|
+
"content": self.content,
|
|
205
|
+
"location": self.location,
|
|
206
|
+
"time_of_day": self.time_of_day,
|
|
207
|
+
"tags": self.tags,
|
|
208
|
+
"metadata": self.metadata,
|
|
209
|
+
"created_at": self.created_at,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@dataclass
|
|
214
|
+
class DramaCharacter:
|
|
215
|
+
"""短剧角色"""
|
|
216
|
+
id: str
|
|
217
|
+
drama_id: str
|
|
218
|
+
name: str
|
|
219
|
+
role: str = "supporting"
|
|
220
|
+
actor: str = ""
|
|
221
|
+
description: str = ""
|
|
222
|
+
personality: str = ""
|
|
223
|
+
avatar_url: str = ""
|
|
224
|
+
tags: List[str] = field(default_factory=list)
|
|
225
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
226
|
+
created_at: float = 0.0
|
|
227
|
+
|
|
228
|
+
def to_dict(self) -> dict:
|
|
229
|
+
return {
|
|
230
|
+
"id": self.id,
|
|
231
|
+
"drama_id": self.drama_id,
|
|
232
|
+
"name": self.name,
|
|
233
|
+
"role": self.role,
|
|
234
|
+
"actor": self.actor,
|
|
235
|
+
"description": self.description,
|
|
236
|
+
"personality": self.personality,
|
|
237
|
+
"avatar_url": self.avatar_url,
|
|
238
|
+
"tags": self.tags,
|
|
239
|
+
"metadata": self.metadata,
|
|
240
|
+
"created_at": self.created_at,
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@dataclass
|
|
245
|
+
class DramaLine:
|
|
246
|
+
"""短剧台词(可关联为记忆)"""
|
|
247
|
+
id: str
|
|
248
|
+
drama_id: str
|
|
249
|
+
line_text: str
|
|
250
|
+
scene_id: str = ""
|
|
251
|
+
character_id: str = ""
|
|
252
|
+
character_name: str = ""
|
|
253
|
+
context: str = ""
|
|
254
|
+
episode: int = 0
|
|
255
|
+
timestamp: str = ""
|
|
256
|
+
is_classic: bool = False
|
|
257
|
+
memory_id: str = ""
|
|
258
|
+
tags: List[str] = field(default_factory=list)
|
|
259
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
260
|
+
created_at: float = 0.0
|
|
261
|
+
|
|
262
|
+
def to_dict(self) -> dict:
|
|
263
|
+
return {
|
|
264
|
+
"id": self.id,
|
|
265
|
+
"drama_id": self.drama_id,
|
|
266
|
+
"scene_id": self.scene_id,
|
|
267
|
+
"character_id": self.character_id,
|
|
268
|
+
"character_name": self.character_name,
|
|
269
|
+
"line_text": self.line_text,
|
|
270
|
+
"context": self.context,
|
|
271
|
+
"episode": self.episode,
|
|
272
|
+
"timestamp": self.timestamp,
|
|
273
|
+
"is_classic": self.is_classic,
|
|
274
|
+
"memory_id": self.memory_id,
|
|
275
|
+
"tags": self.tags,
|
|
276
|
+
"metadata": self.metadata,
|
|
277
|
+
"created_at": self.created_at,
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# ---------------------------------------------------------------------------
|
|
282
|
+
# 入参归一助手(v6.1.2)
|
|
283
|
+
# ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
def coerce_memory_id(value: Any) -> Optional[str]:
|
|
286
|
+
"""把 memory_id 入参归一为字符串,识别失败返回 None。
|
|
287
|
+
|
|
288
|
+
为什么需要它:`mf.add()` 返回的是 `MemoryEntry` 对象,用户很自然地把它原样
|
|
289
|
+
传回其它接口(如 `list_evolutions(m.add(...))`)。历史上各处写的是
|
|
290
|
+
`if not memory_id or len(memory_id) > 128`,对对象求 `len()` 直接抛
|
|
291
|
+
`TypeError: object of type 'MemoryEntry' has no len()`——对外表现为崩溃。
|
|
292
|
+
v6.1.1 已在三个入口(learn_from_outcome / follow_up_create / forget_explain)
|
|
293
|
+
修过这类问题,但每个模块各留了一份私有副本;v6.1.2 把规则收敛到此处的唯一
|
|
294
|
+
实现,供 storage 的演变/版本接口共用。
|
|
295
|
+
|
|
296
|
+
接受:
|
|
297
|
+
- `str`:原样返回;
|
|
298
|
+
- 带 `id` 字符串属性的对象(`MemoryEntry`、`ExperienceCase` 等):返回其 `id`。
|
|
299
|
+
其余(int / list / dict / None / 空串 / 超长)一律返回 None,由调用方按各自的
|
|
300
|
+
错误约定处理(返回 error dict 或空列表),不再抛 TypeError。
|
|
301
|
+
"""
|
|
302
|
+
if isinstance(value, str):
|
|
303
|
+
candidate = value
|
|
304
|
+
else:
|
|
305
|
+
try:
|
|
306
|
+
inner = getattr(value, "id", None)
|
|
307
|
+
except Exception: # noqa: BLE001 病态对象(属性访问抛异常)与非法类型同等对待
|
|
308
|
+
return None
|
|
309
|
+
if not isinstance(inner, str):
|
|
310
|
+
return None
|
|
311
|
+
candidate = inner
|
|
312
|
+
if not candidate or len(candidate) > 128:
|
|
313
|
+
return None
|
|
314
|
+
return candidate
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MindForge 版本号单一来源
|
|
3
|
+
========================
|
|
4
|
+
|
|
5
|
+
历史问题:`__version__` 的真值散落在 5 处(顶层包、core/storage.py、
|
|
6
|
+
core/mindforge.py、cli/main.py、mcp/server.py),每处都带一份硬编码兜底。
|
|
7
|
+
发版时要改 5 次,漏改就会出现「顶层 5.6.1 / storage 5.6.0」这类漂移。
|
|
8
|
+
|
|
9
|
+
本模块把版本号收敛为唯一真值:
|
|
10
|
+
|
|
11
|
+
- 运行时代码一律从这里 import,不再自带硬编码兜底;
|
|
12
|
+
- `pyproject.toml` 通过 `[tool.setuptools.dynamic] version = {attr =
|
|
13
|
+
"mindforge.core.version.__version__"}` 读取同一份值,打包元数据与运行时
|
|
14
|
+
完全一致。
|
|
15
|
+
|
|
16
|
+
发版清单:只改本文件的 `__version__`(与 `VERSION_INFO`),其余位置
|
|
17
|
+
(pyproject 动态版本、CLI/MCP/API、README/官网徽章)全部自动跟随。
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from typing import Tuple
|
|
21
|
+
|
|
22
|
+
#: 当前发布版本(唯一真值)
|
|
23
|
+
__version__: str = "6.4.1"
|
|
24
|
+
|
|
25
|
+
#: 结构化版本元组,便于比较(如 `VERSION_INFO >= (6, 1, 0)`)
|
|
26
|
+
VERSION_INFO: Tuple[int, int, int] = (6, 4, 1)
|
|
27
|
+
|
|
28
|
+
#: 下一个大版本的目标版本号(供 v6 端口预留引用)
|
|
29
|
+
NEXT_MAJOR_TARGET: str = "7.0.0"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_version() -> str:
|
|
33
|
+
"""返回当前版本号字符串(等价于 `__version__`)。
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
形如 "6.1.2" 的 semver 字符串。
|
|
37
|
+
"""
|
|
38
|
+
return __version__
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_version_info() -> Tuple[int, int, int]:
|
|
42
|
+
"""返回结构化版本元组。
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
形如 (6, 1, 2) 的三元整数元组。
|
|
46
|
+
"""
|
|
47
|
+
return VERSION_INFO
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"__version__",
|
|
52
|
+
"VERSION_INFO",
|
|
53
|
+
"NEXT_MAJOR_TARGET",
|
|
54
|
+
"get_version",
|
|
55
|
+
"get_version_info",
|
|
56
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""MindForge 常驻守护进程(daemon)——毫秒级调用路径。
|
|
2
|
+
|
|
3
|
+
背景(2026-11-08 延迟实测):引擎直连 2-3ms,但 CLI 子进程路径 698ms——
|
|
4
|
+
差距全部来自 Python 解释器启动与导入开销。daemon 把引擎实例常驻在
|
|
5
|
+
一个长生命周期进程里,宿主(dsh 插件等)通过 stdin/stdout 行式 JSON
|
|
6
|
+
协议调用,消除每次调用的进程启动税。
|
|
7
|
+
|
|
8
|
+
协议(v6.4.0):
|
|
9
|
+
请求(stdin,每行一个 JSON):
|
|
10
|
+
{"id": <int|str>, "cmd": "<command>", "args": {<kwargs>}}
|
|
11
|
+
响应(stdout,每行一个 JSON,立即可 flush):
|
|
12
|
+
{"id": ..., "ok": true, "data": {...}} 成功
|
|
13
|
+
{"id": ..., "ok": false, "error": "<msg>"} 失败(进程不退出)
|
|
14
|
+
生命周期:
|
|
15
|
+
"ping" → {"pong": true, "pid": ..., "uptime_ms": ...}
|
|
16
|
+
"shutdown" → 优雅退出(退出码 0)
|
|
17
|
+
EOF → 退出
|
|
18
|
+
stderr: 启动横幅与日志(stdout 保持纯 JSON 行,严禁污染)
|
|
19
|
+
|
|
20
|
+
命令集(v1,输出契约对齐 CLI --json):
|
|
21
|
+
add / search / memory-recall / memory-context / stats /
|
|
22
|
+
compress / compress-list / decompress / adaptive-search
|
|
23
|
+
|
|
24
|
+
设计原则:
|
|
25
|
+
- 单线程顺序处理:v1 目标是正确与低延迟,agent 场景以顺序调用为主
|
|
26
|
+
- 每请求独立 try/except:单条请求异常不杀进程
|
|
27
|
+
- 引擎实例只建一次、永不 close(daemon 生命周期 = 引擎生命周期)
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import os
|
|
33
|
+
import sys
|
|
34
|
+
import time
|
|
35
|
+
|
|
36
|
+
from mindforge.core.types import (
|
|
37
|
+
Importance,
|
|
38
|
+
MemoryLayer,
|
|
39
|
+
MemoryType,
|
|
40
|
+
PrivacyLevel,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
__all__ = ["run_daemon"]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _make_serializer():
|
|
47
|
+
"""引擎对象 → CLI --json 兼容 dict 的转换集合(对齐 cli/main.py 契约)。"""
|
|
48
|
+
|
|
49
|
+
def add(entry):
|
|
50
|
+
return {
|
|
51
|
+
"status": "ok",
|
|
52
|
+
"id": entry.id,
|
|
53
|
+
"content": entry.content,
|
|
54
|
+
"category": entry.category,
|
|
55
|
+
"tags": entry.tags,
|
|
56
|
+
"layer": entry.layer.value,
|
|
57
|
+
"importance": entry.importance.value
|
|
58
|
+
if getattr(entry.importance, "value", None)
|
|
59
|
+
else str(entry.importance),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
def search(result):
|
|
63
|
+
return {
|
|
64
|
+
"chunks": [
|
|
65
|
+
{
|
|
66
|
+
"memory_id": c.memory_id,
|
|
67
|
+
"content": c.content,
|
|
68
|
+
"category": c.category,
|
|
69
|
+
"layer": c.layer.value,
|
|
70
|
+
"relevance_score": c.relevance_score,
|
|
71
|
+
"tags": c.tags,
|
|
72
|
+
"score_breakdown": c.score_breakdown,
|
|
73
|
+
}
|
|
74
|
+
for c in result.chunks
|
|
75
|
+
],
|
|
76
|
+
"total_found": result.total_found,
|
|
77
|
+
"query_time_ms": result.query_time_ms,
|
|
78
|
+
"strategy_used": result.strategy_used,
|
|
79
|
+
"token_estimate": result.token_estimate,
|
|
80
|
+
"layers_used": result.layers_used,
|
|
81
|
+
"approximate": getattr(result, "approximate", False),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return add, search
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def run_daemon(db_path: str, key_file: str | None = None, encrypted: bool = False) -> int:
|
|
88
|
+
"""启动常驻守护进程主循环。返回退出码。"""
|
|
89
|
+
from mindforge.core.types import MemoryConfig
|
|
90
|
+
from mindforge.core.mindforge import MindForge
|
|
91
|
+
|
|
92
|
+
t_start = time.time()
|
|
93
|
+
cm = MindForge(
|
|
94
|
+
config=MemoryConfig(db_path=db_path, key_file=key_file, encrypted=encrypted)
|
|
95
|
+
)
|
|
96
|
+
add_ser, search_ser = _make_serializer()
|
|
97
|
+
taxonomy = None # lazy:add 缺省分类时才建
|
|
98
|
+
|
|
99
|
+
print(
|
|
100
|
+
json.dumps(
|
|
101
|
+
{
|
|
102
|
+
"event": "ready",
|
|
103
|
+
"pid": os.getpid(),
|
|
104
|
+
"db_path": db_path,
|
|
105
|
+
"startup_ms": int((time.time() - t_start) * 1000),
|
|
106
|
+
},
|
|
107
|
+
ensure_ascii=False,
|
|
108
|
+
),
|
|
109
|
+
file=sys.stderr,
|
|
110
|
+
flush=True,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def cmd_add(args):
|
|
114
|
+
nonlocal taxonomy
|
|
115
|
+
content = args["content"]
|
|
116
|
+
category = args.get("category")
|
|
117
|
+
tags = args.get("tags")
|
|
118
|
+
if not category or not tags:
|
|
119
|
+
if taxonomy is None:
|
|
120
|
+
from mindforge.modules.categorizer import TaxonomyManager
|
|
121
|
+
|
|
122
|
+
taxonomy = TaxonomyManager()
|
|
123
|
+
if not category:
|
|
124
|
+
category = taxonomy.suggest_category(content)
|
|
125
|
+
if not tags:
|
|
126
|
+
tags = taxonomy.suggest_tags(content)
|
|
127
|
+
entry = cm.add(
|
|
128
|
+
content=content,
|
|
129
|
+
category=category,
|
|
130
|
+
tags=tags,
|
|
131
|
+
privacy=PrivacyLevel.from_string(args.get("privacy", "INTERNAL")),
|
|
132
|
+
importance=Importance.from_string(args.get("importance", "MEDIUM")),
|
|
133
|
+
memory_type=MemoryType.from_string(args.get("type", "text")),
|
|
134
|
+
layer=MemoryLayer.from_string(args.get("layer", "short_term")),
|
|
135
|
+
source_session=args.get("session", ""),
|
|
136
|
+
source_agent=args.get("agent", ""),
|
|
137
|
+
)
|
|
138
|
+
return add_ser(entry)
|
|
139
|
+
|
|
140
|
+
def cmd_search(args):
|
|
141
|
+
result = cm.search(
|
|
142
|
+
query=args["query"],
|
|
143
|
+
max_results=args.get("limit", 10),
|
|
144
|
+
min_relevance=0.2,
|
|
145
|
+
agent_id=args.get("agent", ""),
|
|
146
|
+
session_id=args.get("session", ""),
|
|
147
|
+
use_embedding=args.get("use_embedding", True),
|
|
148
|
+
explain=args.get("explain", False),
|
|
149
|
+
)
|
|
150
|
+
return search_ser(result)
|
|
151
|
+
|
|
152
|
+
def cmd_memory_recall(args):
|
|
153
|
+
return cm.memory_recall(
|
|
154
|
+
args["agent"], args["query"],
|
|
155
|
+
args.get("top_k", 10), args.get("days", 30),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def cmd_memory_context(args):
|
|
159
|
+
return cm.memory_context(
|
|
160
|
+
args["agent"], args["query"], args.get("max_tokens", 4000)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def cmd_stats(args):
|
|
164
|
+
return cm.detailed_stats()
|
|
165
|
+
|
|
166
|
+
def cmd_compress(args):
|
|
167
|
+
return cm.compress_memories(
|
|
168
|
+
threshold=args.get("threshold", 0.5),
|
|
169
|
+
dry_run=args.get("dry_run", False),
|
|
170
|
+
categories=[args["category"]] if args.get("category") else None,
|
|
171
|
+
min_cluster_size=args.get("min_cluster_size", 2),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def cmd_compress_list(args):
|
|
175
|
+
items = cm.list_compressions()
|
|
176
|
+
return {"total": len(items), "clusters": items}
|
|
177
|
+
|
|
178
|
+
def cmd_decompress(args):
|
|
179
|
+
return cm.decompress_cluster(args["cluster_id"])
|
|
180
|
+
|
|
181
|
+
def cmd_adaptive_search(args):
|
|
182
|
+
return cm.adaptive_search(
|
|
183
|
+
query=args["query"],
|
|
184
|
+
session_id=args.get("session", "cli"),
|
|
185
|
+
max_results=args.get("limit", 10),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
handlers = {
|
|
189
|
+
"add": cmd_add,
|
|
190
|
+
"search": cmd_search,
|
|
191
|
+
"memory-recall": cmd_memory_recall,
|
|
192
|
+
"memory-context": cmd_memory_context,
|
|
193
|
+
"stats": cmd_stats,
|
|
194
|
+
"compress": cmd_compress,
|
|
195
|
+
"compress-list": cmd_compress_list,
|
|
196
|
+
"decompress": cmd_decompress,
|
|
197
|
+
"adaptive-search": cmd_adaptive_search,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
for line in sys.stdin:
|
|
201
|
+
line = line.strip()
|
|
202
|
+
if not line:
|
|
203
|
+
continue
|
|
204
|
+
try:
|
|
205
|
+
req = json.loads(line)
|
|
206
|
+
rid = req.get("id")
|
|
207
|
+
cmd = req.get("cmd", "")
|
|
208
|
+
args = req.get("args", {}) or {}
|
|
209
|
+
except json.JSONDecodeError as e:
|
|
210
|
+
sys.stdout.write(
|
|
211
|
+
json.dumps({"id": None, "ok": False, "error": f"bad json: {e}"}) + "\n"
|
|
212
|
+
)
|
|
213
|
+
sys.stdout.flush()
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
if cmd == "ping":
|
|
217
|
+
sys.stdout.write(
|
|
218
|
+
json.dumps(
|
|
219
|
+
{
|
|
220
|
+
"id": rid,
|
|
221
|
+
"ok": True,
|
|
222
|
+
"data": {
|
|
223
|
+
"pong": True,
|
|
224
|
+
"pid": os.getpid(),
|
|
225
|
+
"uptime_ms": int((time.time() - t_start) * 1000),
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
ensure_ascii=False,
|
|
229
|
+
)
|
|
230
|
+
+ "\n"
|
|
231
|
+
)
|
|
232
|
+
sys.stdout.flush()
|
|
233
|
+
continue
|
|
234
|
+
if cmd == "shutdown":
|
|
235
|
+
sys.stdout.write(
|
|
236
|
+
json.dumps({"id": rid, "ok": True, "data": {"bye": True}}) + "\n"
|
|
237
|
+
)
|
|
238
|
+
sys.stdout.flush()
|
|
239
|
+
break
|
|
240
|
+
|
|
241
|
+
handler = handlers.get(cmd)
|
|
242
|
+
if handler is None:
|
|
243
|
+
sys.stdout.write(
|
|
244
|
+
json.dumps(
|
|
245
|
+
{
|
|
246
|
+
"id": rid,
|
|
247
|
+
"ok": False,
|
|
248
|
+
"error": f"unknown cmd: {cmd}(支持: {sorted(handlers) + ['ping', 'shutdown']})",
|
|
249
|
+
},
|
|
250
|
+
ensure_ascii=False,
|
|
251
|
+
)
|
|
252
|
+
+ "\n"
|
|
253
|
+
)
|
|
254
|
+
sys.stdout.flush()
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
data = handler(args)
|
|
259
|
+
sys.stdout.write(
|
|
260
|
+
json.dumps({"id": rid, "ok": True, "data": data}, ensure_ascii=False)
|
|
261
|
+
+ "\n"
|
|
262
|
+
)
|
|
263
|
+
except Exception as e: # 单请求失败不杀进程
|
|
264
|
+
sys.stdout.write(
|
|
265
|
+
json.dumps(
|
|
266
|
+
{"id": rid, "ok": False, "error": f"{type(e).__name__}: {e}"},
|
|
267
|
+
ensure_ascii=False,
|
|
268
|
+
)
|
|
269
|
+
+ "\n"
|
|
270
|
+
)
|
|
271
|
+
sys.stdout.flush()
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
cm.close()
|
|
275
|
+
except Exception:
|
|
276
|
+
pass
|
|
277
|
+
return 0
|