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.
Files changed (64) hide show
  1. MindForge.py +20 -0
  2. mindforge/__init__.py +111 -0
  3. mindforge/adapters/__init__.py +18 -0
  4. mindforge/adapters/claude_adapter.py +84 -0
  5. mindforge/adapters/crewai_memory.py +76 -0
  6. mindforge/adapters/generic_api.py +183 -0
  7. mindforge/adapters/langgraph_store.py +463 -0
  8. mindforge/adapters/openclaw_adapter.py +91 -0
  9. mindforge/api/__init__.py +1 -0
  10. mindforge/api/server.py +1567 -0
  11. mindforge/cli/__init__.py +1 -0
  12. mindforge/cli/main.py +13607 -0
  13. mindforge/core/__init__.py +37 -0
  14. mindforge/core/embedding.py +847 -0
  15. mindforge/core/encryption.py +597 -0
  16. mindforge/core/indexer.py +363 -0
  17. mindforge/core/mindforge.py +6298 -0
  18. mindforge/core/paths.py +43 -0
  19. mindforge/core/query.py +501 -0
  20. mindforge/core/storage.py +18656 -0
  21. mindforge/core/types.py +314 -0
  22. mindforge/core/version.py +56 -0
  23. mindforge/daemon/__init__.py +0 -0
  24. mindforge/daemon/server.py +277 -0
  25. mindforge/mcp/__init__.py +4 -0
  26. mindforge/mcp/server.py +2559 -0
  27. mindforge/modules/__init__.py +138 -0
  28. mindforge/modules/categorizer.py +151 -0
  29. mindforge/modules/conflict_detector.py +356 -0
  30. mindforge/modules/connectors.py +331 -0
  31. mindforge/modules/context_window.py +337 -0
  32. mindforge/modules/event_bus.py +572 -0
  33. mindforge/modules/evolution.py +210 -0
  34. mindforge/modules/experience.py +402 -0
  35. mindforge/modules/federated.py +722 -0
  36. mindforge/modules/federated_acl.py +393 -0
  37. mindforge/modules/follow_up.py +315 -0
  38. mindforge/modules/hybrid_search.py +565 -0
  39. mindforge/modules/importer.py +374 -0
  40. mindforge/modules/integrator.py +196 -0
  41. mindforge/modules/intent_router.py +359 -0
  42. mindforge/modules/knowledge_graph.py +395 -0
  43. mindforge/modules/memory_compressor.py +592 -0
  44. mindforge/modules/memory_decay.py +474 -0
  45. mindforge/modules/memory_passport.py +250 -0
  46. mindforge/modules/multi_agent.py +534 -0
  47. mindforge/modules/multimodal.py +304 -0
  48. mindforge/modules/outcome_learning.py +186 -0
  49. mindforge/modules/personality.py +275 -0
  50. mindforge/modules/principled_forgetting.py +258 -0
  51. mindforge/modules/privacy.py +736 -0
  52. mindforge/modules/recall.py +263 -0
  53. mindforge/modules/session_focus.py +305 -0
  54. mindforge/modules/share_conflict.py +521 -0
  55. mindforge/modules/skill_extractor.py +340 -0
  56. mindforge/modules/skill_pack.py +205 -0
  57. mindforge/modules/timeline_viz.py +206 -0
  58. mindforge/modules/v6.py +931 -0
  59. mindforge_memory-6.4.1.dist-info/METADATA +281 -0
  60. mindforge_memory-6.4.1.dist-info/RECORD +64 -0
  61. mindforge_memory-6.4.1.dist-info/WHEEL +5 -0
  62. mindforge_memory-6.4.1.dist-info/entry_points.txt +3 -0
  63. mindforge_memory-6.4.1.dist-info/licenses/LICENSE +21 -0
  64. 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 包"""