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
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""LangGraph Store 适配器(v5.8.4 新增)。
|
|
3
|
+
|
|
4
|
+
将 MindForge 暴露为 LangGraph BaseStore 风格的跨线程长期记忆 Store,且不
|
|
5
|
+
引入强制 LangGraph 依赖。返回对象同时支持 LangGraph 的属性访问和原有映射
|
|
6
|
+
访问(例如 ``item.value`` 与 ``item["value"]``)。
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
import asyncio
|
|
12
|
+
import json
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from langgraph.store.base import BaseStore as _BaseStore # type: ignore
|
|
18
|
+
_LANGGRAPH_AVAILABLE = True
|
|
19
|
+
except Exception: # pragma: no cover - 无 langgraph 环境回退
|
|
20
|
+
_BaseStore = object # type: ignore
|
|
21
|
+
_LANGGRAPH_AVAILABLE = False
|
|
22
|
+
|
|
23
|
+
KeyTagPrefix = "mf_key:"
|
|
24
|
+
NS_SEP = "/"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _ns_str(namespace: Tuple[str, ...]) -> str:
|
|
28
|
+
if isinstance(namespace, str):
|
|
29
|
+
return namespace
|
|
30
|
+
return NS_SEP.join(str(x) for x in namespace)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _as_namespace(namespace: Any) -> Tuple[str, ...]:
|
|
34
|
+
if namespace is None:
|
|
35
|
+
return ()
|
|
36
|
+
if isinstance(namespace, str):
|
|
37
|
+
return (namespace,) if namespace else ()
|
|
38
|
+
return tuple(str(part) for part in namespace)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _as_datetime(value: Any) -> datetime:
|
|
42
|
+
if isinstance(value, datetime):
|
|
43
|
+
return value if value.tzinfo else value.replace(tzinfo=timezone.utc)
|
|
44
|
+
try:
|
|
45
|
+
return datetime.fromtimestamp(float(value), tz=timezone.utc)
|
|
46
|
+
except (TypeError, ValueError, OverflowError, OSError):
|
|
47
|
+
return datetime.fromtimestamp(0, tz=timezone.utc)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _StoreItem(dict):
|
|
51
|
+
"""符合 Item/SearchItem 属性协议,同时兼容原有 dict 风格调用。"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, namespace, key, value, created_at, updated_at,
|
|
54
|
+
memory_id=None, score=None):
|
|
55
|
+
super().__init__(
|
|
56
|
+
namespace=tuple(namespace),
|
|
57
|
+
key=str(key),
|
|
58
|
+
value=value,
|
|
59
|
+
created_at=_as_datetime(created_at),
|
|
60
|
+
updated_at=_as_datetime(updated_at),
|
|
61
|
+
)
|
|
62
|
+
if memory_id:
|
|
63
|
+
self["memory_id"] = memory_id
|
|
64
|
+
if score is not None:
|
|
65
|
+
self["score"] = score
|
|
66
|
+
|
|
67
|
+
def __getattr__(self, name):
|
|
68
|
+
try:
|
|
69
|
+
return self[name]
|
|
70
|
+
except KeyError as exc:
|
|
71
|
+
raise AttributeError(name) from exc
|
|
72
|
+
|
|
73
|
+
def dict(self):
|
|
74
|
+
return dict(self)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class MindForgeStore(_BaseStore):
|
|
78
|
+
"""LangGraph BaseStore 协议的 MindForge 后端。"""
|
|
79
|
+
|
|
80
|
+
_LIST_PAGE_SIZE = 500
|
|
81
|
+
|
|
82
|
+
def __init__(self, mindforge, default_category: str = "langgraph"):
|
|
83
|
+
self.mf = mindforge
|
|
84
|
+
self.default_category = default_category
|
|
85
|
+
|
|
86
|
+
def _key_tag(self, key: str) -> str:
|
|
87
|
+
return KeyTagPrefix + str(key)
|
|
88
|
+
|
|
89
|
+
def _category(self, namespace: Tuple[str, ...]) -> str:
|
|
90
|
+
return _ns_str(namespace) or self.default_category
|
|
91
|
+
|
|
92
|
+
def _resolve_ids(self, namespace: Tuple[str, ...], key: str) -> List[str]:
|
|
93
|
+
"""按完整 namespace+tag 定位所有重复的 memory_id,不读取明文。"""
|
|
94
|
+
try:
|
|
95
|
+
hits = self.mf.search_by_tag(
|
|
96
|
+
self._key_tag(key), category=self._category(namespace), limit=100
|
|
97
|
+
)
|
|
98
|
+
except AttributeError:
|
|
99
|
+
return []
|
|
100
|
+
ids = list(dict.fromkeys(
|
|
101
|
+
str(getattr(hit, "id", "")) for hit in hits if getattr(hit, "id", None)
|
|
102
|
+
))
|
|
103
|
+
requested = _as_namespace(namespace)
|
|
104
|
+
resolved = []
|
|
105
|
+
for memory_id in ids:
|
|
106
|
+
entry = self._entry_for_id(memory_id)
|
|
107
|
+
if entry is not None and self._namespace_for_entry(entry) == requested:
|
|
108
|
+
resolved.append(memory_id)
|
|
109
|
+
return resolved
|
|
110
|
+
|
|
111
|
+
def _resolve_id(self, namespace: Tuple[str, ...], key: str) -> Optional[str]:
|
|
112
|
+
ids = self._resolve_ids(namespace, key)
|
|
113
|
+
return ids[0] if ids else None
|
|
114
|
+
|
|
115
|
+
def _entry_for_id(self, memory_id: str):
|
|
116
|
+
try:
|
|
117
|
+
return self.mf._storage.get_memory(memory_id)
|
|
118
|
+
except (AttributeError, TypeError):
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
def _namespace_for_entry(self, entry) -> Tuple[str, ...]:
|
|
122
|
+
"""优先从适配器写入的 source_session 还原 tuple,兼容旧分类行。"""
|
|
123
|
+
raw = getattr(entry, "source_session", "") or ""
|
|
124
|
+
if raw:
|
|
125
|
+
try:
|
|
126
|
+
value = ast.literal_eval(raw)
|
|
127
|
+
if isinstance(value, (tuple, list)) and all(
|
|
128
|
+
isinstance(part, str) for part in value):
|
|
129
|
+
return tuple(value)
|
|
130
|
+
except (ValueError, SyntaxError, TypeError):
|
|
131
|
+
pass
|
|
132
|
+
category = str(getattr(entry, "category", "") or "")
|
|
133
|
+
if category == self.default_category and not raw:
|
|
134
|
+
return ()
|
|
135
|
+
return tuple(category.split(NS_SEP)) if category else ()
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def _key_for_entry(entry) -> str:
|
|
139
|
+
for tag in (getattr(entry, "tags", []) or []):
|
|
140
|
+
tag = str(tag)
|
|
141
|
+
if tag.startswith(KeyTagPrefix):
|
|
142
|
+
return tag[len(KeyTagPrefix):]
|
|
143
|
+
return ""
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def _decode_value(content: str) -> Dict[str, Any]:
|
|
147
|
+
if not content:
|
|
148
|
+
return {}
|
|
149
|
+
try:
|
|
150
|
+
value = json.loads(content)
|
|
151
|
+
except (ValueError, TypeError):
|
|
152
|
+
return {"value": content}
|
|
153
|
+
return value if isinstance(value, dict) else {"value": value}
|
|
154
|
+
|
|
155
|
+
def _make_item(self, namespace, key, value, entry=None,
|
|
156
|
+
memory_id=None, score=None):
|
|
157
|
+
created_at = getattr(entry, "created_at", 0) if entry is not None else 0
|
|
158
|
+
updated_at = getattr(entry, "updated_at", created_at) if entry is not None else created_at
|
|
159
|
+
return _StoreItem(
|
|
160
|
+
namespace=namespace,
|
|
161
|
+
key=key,
|
|
162
|
+
value=value,
|
|
163
|
+
created_at=created_at,
|
|
164
|
+
updated_at=updated_at,
|
|
165
|
+
memory_id=memory_id or getattr(entry, "id", None),
|
|
166
|
+
score=score,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def _iter_entries(self) -> Iterable[Any]:
|
|
170
|
+
offset = 0
|
|
171
|
+
while True:
|
|
172
|
+
try:
|
|
173
|
+
page = self.mf.list(
|
|
174
|
+
category=None, limit=self._LIST_PAGE_SIZE, offset=offset
|
|
175
|
+
) or []
|
|
176
|
+
except AttributeError:
|
|
177
|
+
return
|
|
178
|
+
if not page:
|
|
179
|
+
return
|
|
180
|
+
yield from page
|
|
181
|
+
if len(page) < self._LIST_PAGE_SIZE:
|
|
182
|
+
return
|
|
183
|
+
offset += len(page)
|
|
184
|
+
|
|
185
|
+
@staticmethod
|
|
186
|
+
def _namespace_matches(namespace, prefix) -> bool:
|
|
187
|
+
prefix = _as_namespace(prefix)
|
|
188
|
+
return len(namespace) >= len(prefix) and namespace[:len(prefix)] == prefix
|
|
189
|
+
|
|
190
|
+
@staticmethod
|
|
191
|
+
def _matches_filter(value: Dict[str, Any], filters: Optional[Dict[str, Any]]) -> bool:
|
|
192
|
+
if not filters:
|
|
193
|
+
return True
|
|
194
|
+
operators = {
|
|
195
|
+
"$eq": lambda actual, expected: actual == expected,
|
|
196
|
+
"$ne": lambda actual, expected: actual != expected,
|
|
197
|
+
"$gt": lambda actual, expected: actual is not None and actual > expected,
|
|
198
|
+
"$gte": lambda actual, expected: actual is not None and actual >= expected,
|
|
199
|
+
"$lt": lambda actual, expected: actual is not None and actual < expected,
|
|
200
|
+
"$lte": lambda actual, expected: actual is not None and actual <= expected,
|
|
201
|
+
}
|
|
202
|
+
for dotted_key, expected in filters.items():
|
|
203
|
+
actual = value
|
|
204
|
+
for part in str(dotted_key).split("."):
|
|
205
|
+
actual = actual.get(part) if isinstance(actual, dict) else None
|
|
206
|
+
if isinstance(expected, dict):
|
|
207
|
+
for operator, operand in expected.items():
|
|
208
|
+
compare = operators.get(operator)
|
|
209
|
+
if compare is None:
|
|
210
|
+
return False
|
|
211
|
+
try:
|
|
212
|
+
if not compare(actual, operand):
|
|
213
|
+
return False
|
|
214
|
+
except (TypeError, ValueError):
|
|
215
|
+
return False
|
|
216
|
+
elif actual != expected:
|
|
217
|
+
return False
|
|
218
|
+
return True
|
|
219
|
+
|
|
220
|
+
@staticmethod
|
|
221
|
+
def _validate_page(limit: int, offset: int) -> None:
|
|
222
|
+
if (isinstance(limit, bool) or not isinstance(limit, int) or limit < 0
|
|
223
|
+
or isinstance(offset, bool) or not isinstance(offset, int) or offset < 0):
|
|
224
|
+
raise ValueError("limit and offset must be non-negative integers")
|
|
225
|
+
|
|
226
|
+
# ---------- BaseStore 协议 ----------
|
|
227
|
+
def put(self, namespace: Tuple[str, ...], key: str,
|
|
228
|
+
value: Optional[Dict[str, Any]], index=None, *, ttl=None) -> None:
|
|
229
|
+
"""写入或删除一条记忆(同 namespace+key 保持幂等)。"""
|
|
230
|
+
namespace = _as_namespace(namespace)
|
|
231
|
+
if value is None:
|
|
232
|
+
self.delete(namespace, key)
|
|
233
|
+
return None
|
|
234
|
+
category = self._category(namespace)
|
|
235
|
+
for old_id in self._resolve_ids(namespace, key):
|
|
236
|
+
self.mf.delete(old_id)
|
|
237
|
+
content = json.dumps(value, ensure_ascii=False, sort_keys=True, default=str)
|
|
238
|
+
self.mf.add(
|
|
239
|
+
content,
|
|
240
|
+
category=category,
|
|
241
|
+
tags=[self._key_tag(key)],
|
|
242
|
+
source_session=str(namespace) if namespace else "",
|
|
243
|
+
)
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
def get(self, namespace: Tuple[str, ...], key: str, *, refresh_ttl=None):
|
|
247
|
+
"""按 namespace+key 精确读取,并返回 Item 兼容对象。"""
|
|
248
|
+
namespace = _as_namespace(namespace)
|
|
249
|
+
category = self._category(namespace)
|
|
250
|
+
memory_id = self._resolve_id(namespace, key)
|
|
251
|
+
if not memory_id:
|
|
252
|
+
return None
|
|
253
|
+
result = self.mf.search(
|
|
254
|
+
self._key_tag(key), max_results=10, categories=[category]
|
|
255
|
+
)
|
|
256
|
+
for chunk in (result.chunks or []):
|
|
257
|
+
chunk_id = getattr(chunk, "memory_id", "") or ""
|
|
258
|
+
if chunk_id != memory_id:
|
|
259
|
+
continue
|
|
260
|
+
value = self._decode_value(getattr(chunk, "content", ""))
|
|
261
|
+
entry = self._entry_for_id(memory_id)
|
|
262
|
+
return self._make_item(namespace, key, value, entry, memory_id)
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
def search(self, namespace_prefix: Tuple[str, ...], query: Optional[str] = None,
|
|
266
|
+
filter: Optional[Dict[str, Any]] = None, limit: int = 10,
|
|
267
|
+
offset: int = 0, *, refresh_ttl=None) -> List[_StoreItem]:
|
|
268
|
+
"""在 namespace 前缀下搜索,并在过滤后执行分页。"""
|
|
269
|
+
self._validate_page(limit, offset)
|
|
270
|
+
if limit == 0:
|
|
271
|
+
return []
|
|
272
|
+
prefix = _as_namespace(namespace_prefix)
|
|
273
|
+
needed = offset + limit
|
|
274
|
+
items: List[_StoreItem] = []
|
|
275
|
+
|
|
276
|
+
if query:
|
|
277
|
+
candidate_limit = max(10, needed)
|
|
278
|
+
while True:
|
|
279
|
+
result = self.mf.search(query, max_results=candidate_limit)
|
|
280
|
+
items = []
|
|
281
|
+
for chunk in (result.chunks or []):
|
|
282
|
+
memory_id = getattr(chunk, "memory_id", "") or ""
|
|
283
|
+
entry = self._entry_for_id(memory_id) if memory_id else None
|
|
284
|
+
namespace = self._namespace_for_entry(entry) if entry else tuple(
|
|
285
|
+
str(getattr(chunk, "category", "") or "").split(NS_SEP)
|
|
286
|
+
)
|
|
287
|
+
if not self._namespace_matches(namespace, prefix):
|
|
288
|
+
continue
|
|
289
|
+
key = self._key_for_entry(entry) if entry else ""
|
|
290
|
+
if not key:
|
|
291
|
+
continue
|
|
292
|
+
value = self._decode_value(getattr(chunk, "content", ""))
|
|
293
|
+
if not self._matches_filter(value, filter):
|
|
294
|
+
continue
|
|
295
|
+
items.append(self._make_item(
|
|
296
|
+
namespace, key, value, entry, memory_id,
|
|
297
|
+
score=getattr(chunk, "relevance_score", None),
|
|
298
|
+
))
|
|
299
|
+
if len(items) >= needed or len(result.chunks or []) < candidate_limit:
|
|
300
|
+
break
|
|
301
|
+
candidate_limit *= 2
|
|
302
|
+
else:
|
|
303
|
+
for entry in self._iter_entries():
|
|
304
|
+
namespace = self._namespace_for_entry(entry)
|
|
305
|
+
if not self._namespace_matches(namespace, prefix):
|
|
306
|
+
continue
|
|
307
|
+
key = self._key_for_entry(entry)
|
|
308
|
+
if not key:
|
|
309
|
+
continue
|
|
310
|
+
item = self.get(namespace, key)
|
|
311
|
+
if item is None or not self._matches_filter(item.value, filter):
|
|
312
|
+
continue
|
|
313
|
+
items.append(item)
|
|
314
|
+
if len(items) >= needed:
|
|
315
|
+
break
|
|
316
|
+
|
|
317
|
+
return items[offset:offset + limit]
|
|
318
|
+
|
|
319
|
+
def delete(self, namespace: Tuple[str, ...], key: str) -> None:
|
|
320
|
+
for memory_id in self._resolve_ids(_as_namespace(namespace), key):
|
|
321
|
+
self.mf.delete(memory_id)
|
|
322
|
+
|
|
323
|
+
def list_namespaces(self, *, prefix=None, suffix=None, max_depth=None,
|
|
324
|
+
limit: int = 100, offset: int = 0) -> List[Tuple[str, ...]]:
|
|
325
|
+
"""列出实际存有记忆的 namespace,支持前缀、后缀和分页。"""
|
|
326
|
+
self._validate_page(limit, offset)
|
|
327
|
+
prefix = _as_namespace(prefix)
|
|
328
|
+
suffix = _as_namespace(suffix)
|
|
329
|
+
if max_depth is not None:
|
|
330
|
+
if (isinstance(max_depth, bool) or not isinstance(max_depth, int)
|
|
331
|
+
or max_depth < 0):
|
|
332
|
+
raise ValueError("max_depth must be a non-negative integer")
|
|
333
|
+
if len(prefix) > max_depth:
|
|
334
|
+
return []
|
|
335
|
+
if limit == 0:
|
|
336
|
+
return []
|
|
337
|
+
found = set()
|
|
338
|
+
for entry in self._iter_entries():
|
|
339
|
+
if not self._key_for_entry(entry):
|
|
340
|
+
continue
|
|
341
|
+
namespace = self._namespace_for_entry(entry)
|
|
342
|
+
if prefix and not self._namespace_matches(namespace, prefix):
|
|
343
|
+
continue
|
|
344
|
+
if suffix and (len(namespace) < len(suffix)
|
|
345
|
+
or namespace[-len(suffix):] != suffix):
|
|
346
|
+
continue
|
|
347
|
+
if max_depth is not None:
|
|
348
|
+
namespace = namespace[:max_depth]
|
|
349
|
+
found.add(namespace)
|
|
350
|
+
ordered = sorted(found)
|
|
351
|
+
return ordered[offset:offset + limit]
|
|
352
|
+
|
|
353
|
+
def list(self, prefix: Tuple[str, ...] = (),
|
|
354
|
+
limit: int = 10, offset: int = 0) -> List[_StoreItem]:
|
|
355
|
+
return self.search(prefix, query=None, limit=limit, offset=offset)
|
|
356
|
+
|
|
357
|
+
def _list_namespaces_op(self, op) -> List[Tuple[str, ...]]:
|
|
358
|
+
prefix = suffix = None
|
|
359
|
+
for condition in (getattr(op, "match_conditions", None) or ()):
|
|
360
|
+
match_type = getattr(condition, "match_type", "")
|
|
361
|
+
match_type = getattr(match_type, "value", match_type)
|
|
362
|
+
path = _as_namespace(getattr(condition, "path", ()))
|
|
363
|
+
if match_type == "prefix":
|
|
364
|
+
prefix = path
|
|
365
|
+
elif match_type == "suffix":
|
|
366
|
+
suffix = path
|
|
367
|
+
else:
|
|
368
|
+
raise ValueError("unsupported namespace match condition")
|
|
369
|
+
return self.list_namespaces(
|
|
370
|
+
prefix=prefix,
|
|
371
|
+
suffix=suffix,
|
|
372
|
+
max_depth=getattr(op, "max_depth", None),
|
|
373
|
+
limit=getattr(op, "limit", 100),
|
|
374
|
+
offset=getattr(op, "offset", 0),
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
# ---------- batch / async ----------
|
|
378
|
+
@staticmethod
|
|
379
|
+
def _unpack_item(item: Any):
|
|
380
|
+
"""兼容 (namespace,key,value) / 带 index 的 tuple / dict。"""
|
|
381
|
+
if isinstance(item, dict):
|
|
382
|
+
namespace = _as_namespace(item.get("namespace"))
|
|
383
|
+
return namespace, str(item["key"]), item.get("value"), item.get("index")
|
|
384
|
+
namespace, key, value = item[0], item[1], item[2]
|
|
385
|
+
index = item[3] if len(item) > 3 else None
|
|
386
|
+
return _as_namespace(namespace), str(key), value, index
|
|
387
|
+
|
|
388
|
+
@staticmethod
|
|
389
|
+
def _op_name(op: Any) -> str:
|
|
390
|
+
return type(op).__name__
|
|
391
|
+
|
|
392
|
+
def batch(self, items: Iterable[Any]) -> List[Any]:
|
|
393
|
+
"""按 LangGraph Op 分发;PutOp(value=None) 表示删除。"""
|
|
394
|
+
out: List[Any] = []
|
|
395
|
+
for item in items:
|
|
396
|
+
op = self._op_name(item)
|
|
397
|
+
if op == "GetOp":
|
|
398
|
+
out.append(self.get(item.namespace, item.key,
|
|
399
|
+
refresh_ttl=getattr(item, "refresh_ttl", None)))
|
|
400
|
+
elif op == "SearchOp":
|
|
401
|
+
out.append(self.search(
|
|
402
|
+
item.namespace_prefix,
|
|
403
|
+
query=getattr(item, "query", None),
|
|
404
|
+
filter=getattr(item, "filter", None),
|
|
405
|
+
limit=getattr(item, "limit", 10),
|
|
406
|
+
offset=getattr(item, "offset", 0),
|
|
407
|
+
refresh_ttl=getattr(item, "refresh_ttl", None),
|
|
408
|
+
))
|
|
409
|
+
elif op == "ListNamespacesOp":
|
|
410
|
+
out.append(self._list_namespaces_op(item))
|
|
411
|
+
elif op == "PutOp":
|
|
412
|
+
namespace, key, value = item.namespace, item.key, item.value
|
|
413
|
+
if value is None:
|
|
414
|
+
self.delete(namespace, key)
|
|
415
|
+
out.append(None)
|
|
416
|
+
else:
|
|
417
|
+
out.append(self.put(namespace, key, value,
|
|
418
|
+
index=getattr(item, "index", None),
|
|
419
|
+
ttl=getattr(item, "ttl", None)))
|
|
420
|
+
elif isinstance(item, (tuple, list, dict)):
|
|
421
|
+
namespace, key, value, index = self._unpack_item(item)
|
|
422
|
+
out.append(self.put(namespace, key, value, index=index))
|
|
423
|
+
else:
|
|
424
|
+
out.append(None)
|
|
425
|
+
return out
|
|
426
|
+
|
|
427
|
+
async def abatch(self, items: Iterable[Any]) -> List[Any]:
|
|
428
|
+
return await asyncio.to_thread(self.batch, list(items))
|
|
429
|
+
|
|
430
|
+
async def aget(self, namespace, key, *, refresh_ttl=None):
|
|
431
|
+
return await asyncio.to_thread(self.get, namespace, key,
|
|
432
|
+
refresh_ttl=refresh_ttl)
|
|
433
|
+
|
|
434
|
+
async def aput(self, namespace, key, value, index=None, *, ttl=None):
|
|
435
|
+
return await asyncio.to_thread(self.put, namespace, key, value, index,
|
|
436
|
+
ttl=ttl)
|
|
437
|
+
|
|
438
|
+
async def asearch(self, namespace_prefix, query=None, filter=None,
|
|
439
|
+
limit=10, offset=0, *, refresh_ttl=None):
|
|
440
|
+
return await asyncio.to_thread(
|
|
441
|
+
self.search, namespace_prefix, query, filter, limit, offset,
|
|
442
|
+
refresh_ttl=refresh_ttl,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
async def adelete(self, namespace, key):
|
|
446
|
+
return await asyncio.to_thread(self.delete, namespace, key)
|
|
447
|
+
|
|
448
|
+
async def alist(self, prefix=(), limit=10, offset=0):
|
|
449
|
+
return await asyncio.to_thread(self.list, prefix, limit, offset)
|
|
450
|
+
|
|
451
|
+
async def alist_namespaces(self, *, prefix=None, suffix=None, max_depth=None,
|
|
452
|
+
limit=100, offset=0):
|
|
453
|
+
return await asyncio.to_thread(
|
|
454
|
+
self.list_namespaces,
|
|
455
|
+
prefix=prefix,
|
|
456
|
+
suffix=suffix,
|
|
457
|
+
max_depth=max_depth,
|
|
458
|
+
limit=limit,
|
|
459
|
+
offset=offset,
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
__all__ = ["MindForgeStore", "_LANGGRAPH_AVAILABLE"]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MindForge OpenClaw 适配器
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Dict, Optional
|
|
6
|
+
|
|
7
|
+
from mindforge.core.version import __version__ as _MF_VERSION
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OpenClawAdapter:
|
|
11
|
+
"""OpenClaw 记忆适配器"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, MindForge):
|
|
14
|
+
self.cm = MindForge
|
|
15
|
+
self.session_id = "openclaw"
|
|
16
|
+
self.agent_id = "openclaw"
|
|
17
|
+
|
|
18
|
+
def store(self, content: str, metadata: Optional[Dict] = None) -> str:
|
|
19
|
+
"""存储记忆"""
|
|
20
|
+
metadata = metadata or {}
|
|
21
|
+
entry = self.cm.add(
|
|
22
|
+
content=content,
|
|
23
|
+
category=metadata.get("category", "general"),
|
|
24
|
+
tags=metadata.get("tags", []),
|
|
25
|
+
source_session=self.session_id,
|
|
26
|
+
source_agent=self.agent_id,
|
|
27
|
+
metadata=metadata,
|
|
28
|
+
)
|
|
29
|
+
return entry.id
|
|
30
|
+
|
|
31
|
+
def recall(self, query: str, limit: int = 10) -> List[Dict]:
|
|
32
|
+
"""召回记忆"""
|
|
33
|
+
result = self.cm.search(
|
|
34
|
+
query=query,
|
|
35
|
+
max_results=limit,
|
|
36
|
+
agent_id=self.agent_id,
|
|
37
|
+
session_id=self.session_id,
|
|
38
|
+
)
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
"id": chunk.memory_id,
|
|
42
|
+
"content": chunk.content,
|
|
43
|
+
"category": chunk.category,
|
|
44
|
+
"relevance": chunk.relevance_score,
|
|
45
|
+
"layer": chunk.layer.value,
|
|
46
|
+
"tags": chunk.tags,
|
|
47
|
+
}
|
|
48
|
+
for chunk in result.chunks
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
def forget(self, memory_id: str) -> bool:
|
|
52
|
+
"""删除记忆"""
|
|
53
|
+
return self.cm.delete(memory_id, self.agent_id, self.session_id)
|
|
54
|
+
|
|
55
|
+
def get_stats(self) -> Dict:
|
|
56
|
+
"""获取统计"""
|
|
57
|
+
return self.cm.stats()
|
|
58
|
+
|
|
59
|
+
def set_session(self, session_id: str):
|
|
60
|
+
"""设置会话"""
|
|
61
|
+
self.session_id = session_id
|
|
62
|
+
|
|
63
|
+
def set_agent(self, agent_id: str):
|
|
64
|
+
"""设置 Agent"""
|
|
65
|
+
self.agent_id = agent_id
|
|
66
|
+
|
|
67
|
+
def consolidate(self):
|
|
68
|
+
"""触发记忆巩固"""
|
|
69
|
+
from mindforge.modules.evolution import MemoryEvolution
|
|
70
|
+
evo = MemoryEvolution(self.cm.storage)
|
|
71
|
+
return evo.consolidate(self.agent_id, self.session_id)
|
|
72
|
+
|
|
73
|
+
def get_personality(self, user_id: str = "default") -> Dict:
|
|
74
|
+
"""获取用户画像"""
|
|
75
|
+
from mindforge.modules.personality import PersonalityEngine
|
|
76
|
+
pe = PersonalityEngine(self.cm.storage)
|
|
77
|
+
return pe.get_recommended_style(user_id)
|
|
78
|
+
|
|
79
|
+
def to_config_dict(self) -> Dict:
|
|
80
|
+
"""输出配置字典格式"""
|
|
81
|
+
return {
|
|
82
|
+
"adapter": "MindForge",
|
|
83
|
+
"version": _MF_VERSION,
|
|
84
|
+
"features": [
|
|
85
|
+
"four_layer_memory",
|
|
86
|
+
"knowledge_graph",
|
|
87
|
+
"multimodal",
|
|
88
|
+
"personality",
|
|
89
|
+
"federated",
|
|
90
|
+
],
|
|
91
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MindForge API 包"""
|