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,304 @@
1
+ """
2
+ MindForge v5.0 多模态记忆模块
3
+ 支持文本、图像、音频、代码、结构化数据
4
+ """
5
+
6
+ import base64
7
+ import json
8
+ from dataclasses import dataclass, field
9
+ from enum import Enum
10
+ from typing import Dict, List, Optional, Any, Tuple
11
+
12
+
13
+ class MultimodalType(Enum):
14
+ """多模态类型"""
15
+ TEXT = "text"
16
+ IMAGE = "image"
17
+ AUDIO = "audio"
18
+ CODE = "code"
19
+ STRUCTURED = "structured"
20
+ MULTIMODAL = "multimodal"
21
+
22
+
23
+ @dataclass
24
+ class MultimodalContent:
25
+ """多模态内容"""
26
+ content_type: MultimodalType
27
+ data: str = ""
28
+ metadata: Dict[str, Any] = field(default_factory=dict)
29
+ embedding: Optional[List[float]] = None
30
+ text_representation: str = ""
31
+
32
+ def to_dict(self) -> dict:
33
+ return {
34
+ "content_type": self.content_type.value,
35
+ "data": self.data,
36
+ "metadata": self.metadata,
37
+ "text_representation": self.text_representation,
38
+ }
39
+
40
+ @classmethod
41
+ def from_dict(cls, data: dict) -> "MultimodalContent":
42
+ return cls(
43
+ content_type=MultimodalType(data.get("content_type", "text")),
44
+ data=data.get("data", ""),
45
+ metadata=data.get("metadata", {}),
46
+ text_representation=data.get("text_representation", ""),
47
+ )
48
+
49
+
50
+ @dataclass
51
+ class ImageMetadata:
52
+ """图像元数据"""
53
+ width: int = 0
54
+ height: int = 0
55
+ format: str = ""
56
+ size_bytes: int = 0
57
+ caption: str = ""
58
+ objects_detected: List[str] = field(default_factory=list)
59
+ scene_description: str = ""
60
+
61
+ def to_dict(self) -> dict:
62
+ return self.__dict__
63
+
64
+
65
+ @dataclass
66
+ class AudioMetadata:
67
+ """音频元数据"""
68
+ duration_seconds: float = 0.0
69
+ format: str = ""
70
+ sample_rate: int = 0
71
+ transcript: str = ""
72
+ speaker: str = ""
73
+ language: str = ""
74
+
75
+ def to_dict(self) -> dict:
76
+ return self.__dict__
77
+
78
+
79
+ @dataclass
80
+ class CodeMetadata:
81
+ """代码元数据"""
82
+ language: str = ""
83
+ lines_of_code: int = 0
84
+ function_count: int = 0
85
+ class_count: int = 0
86
+ purpose: str = ""
87
+ dependencies: List[str] = field(default_factory=list)
88
+
89
+ def to_dict(self) -> dict:
90
+ return self.__dict__
91
+
92
+
93
+ class MultimodalMemory:
94
+ """多模态记忆管理器"""
95
+
96
+ def __init__(self, storage=None, index=None):
97
+ self.storage = storage
98
+ self.index = index
99
+ self._type_stats: Dict[str, int] = {}
100
+
101
+ def create_text_memory(self, text: str, **kwargs) -> MultimodalContent:
102
+ """创建文本记忆"""
103
+ return MultimodalContent(
104
+ content_type=MultimodalType.TEXT,
105
+ data=text,
106
+ text_representation=text,
107
+ metadata={"word_count": len(text.split()), "char_count": len(text)},
108
+ )
109
+
110
+ def create_image_memory(self,
111
+ image_data: bytes,
112
+ caption: str = "",
113
+ width: int = 0,
114
+ height: int = 0,
115
+ format: str = "",
116
+ objects: Optional[List[str]] = None) -> MultimodalContent:
117
+ """创建图像记忆"""
118
+ # v5.4.7 修复 L-9:限制图像大小(默认 50MB)
119
+ MAX_IMAGE_SIZE = 50 * 1024 * 1024
120
+ if len(image_data) > MAX_IMAGE_SIZE:
121
+ raise ValueError(f"Image too large: {len(image_data)} bytes (max {MAX_IMAGE_SIZE})")
122
+ encoded = base64.b64encode(image_data).decode()
123
+ metadata = ImageMetadata(
124
+ width=width,
125
+ height=height,
126
+ format=format,
127
+ size_bytes=len(image_data),
128
+ caption=caption,
129
+ objects_detected=objects or [],
130
+ )
131
+
132
+ text_repr = f"[图像] {caption} "
133
+ if objects:
134
+ text_repr += f"包含:{', '.join(objects)}"
135
+
136
+ return MultimodalContent(
137
+ content_type=MultimodalType.IMAGE,
138
+ data=encoded,
139
+ metadata=metadata.to_dict(),
140
+ text_representation=text_repr,
141
+ )
142
+
143
+ def create_audio_memory(self,
144
+ audio_data: bytes,
145
+ transcript: str = "",
146
+ duration: float = 0.0,
147
+ format: str = "",
148
+ speaker: str = "",
149
+ language: str = "") -> MultimodalContent:
150
+ """创建音频记忆"""
151
+ # v5.4.7 修复 L-9:限制音频大小(默认 100MB)
152
+ MAX_AUDIO_SIZE = 100 * 1024 * 1024
153
+ if len(audio_data) > MAX_AUDIO_SIZE:
154
+ raise ValueError(f"Audio too large: {len(audio_data)} bytes (max {MAX_AUDIO_SIZE})")
155
+ encoded = base64.b64encode(audio_data).decode()
156
+ metadata = AudioMetadata(
157
+ duration_seconds=duration,
158
+ format=format,
159
+ transcript=transcript,
160
+ speaker=speaker,
161
+ language=language,
162
+ )
163
+
164
+ text_repr = f"[音频] {speaker} 说:{transcript}" if transcript else "[音频]"
165
+
166
+ return MultimodalContent(
167
+ content_type=MultimodalType.AUDIO,
168
+ data=encoded,
169
+ metadata=metadata.to_dict(),
170
+ text_representation=text_repr,
171
+ )
172
+
173
+ def create_code_memory(self,
174
+ code: str,
175
+ language: str = "",
176
+ purpose: str = "",
177
+ dependencies: Optional[List[str]] = None) -> MultimodalContent:
178
+ """创建代码记忆"""
179
+ lines = code.strip().split('\n')
180
+ func_count = sum(1 for line in lines if line.strip().startswith('def ') or line.strip().startswith('function '))
181
+ class_count = sum(1 for line in lines if line.strip().startswith('class '))
182
+
183
+ metadata = CodeMetadata(
184
+ language=language,
185
+ lines_of_code=len(lines),
186
+ function_count=func_count,
187
+ class_count=class_count,
188
+ purpose=purpose,
189
+ dependencies=dependencies or [],
190
+ )
191
+
192
+ text_repr = f"[代码-{language}] {purpose}\n{code[:200]}"
193
+
194
+ return MultimodalContent(
195
+ content_type=MultimodalType.CODE,
196
+ data=code,
197
+ metadata=metadata.to_dict(),
198
+ text_representation=text_repr,
199
+ )
200
+
201
+ def create_structured_memory(self,
202
+ data: Dict,
203
+ schema_name: str = "",
204
+ description: str = "") -> MultimodalContent:
205
+ """创建结构化数据记忆"""
206
+ json_str = json.dumps(data, ensure_ascii=False, indent=2)
207
+ text_repr = f"[结构化数据-{schema_name}] {description}\n键:{', '.join(data.keys())}"
208
+
209
+ return MultimodalContent(
210
+ content_type=MultimodalType.STRUCTURED,
211
+ data=json_str,
212
+ metadata={"schema": schema_name, "key_count": len(data)},
213
+ text_representation=text_repr,
214
+ )
215
+
216
+ def create_multimodal_memory(self,
217
+ contents: List[MultimodalContent],
218
+ description: str = "") -> MultimodalContent:
219
+ """创建复合多模态记忆"""
220
+ all_text = "\n".join(c.text_representation for c in contents)
221
+ combined_data = json.dumps([c.to_dict() for c in contents], ensure_ascii=False)
222
+
223
+ return MultimodalContent(
224
+ content_type=MultimodalType.MULTIMODAL,
225
+ data=combined_data,
226
+ metadata={
227
+ "modality_count": len(contents),
228
+ "modalities": [c.content_type.value for c in contents],
229
+ "description": description,
230
+ },
231
+ text_representation=all_text,
232
+ )
233
+
234
+ def search_multimodal(self,
235
+ query: str,
236
+ content_types: Optional[List[MultimodalType]] = None,
237
+ limit: int = 10) -> List[Tuple[MultimodalContent, float]]:
238
+ """跨模态搜索
239
+
240
+ v6.1.2 修复(两个方向都会出错):`entry.memory_type` 是 MemoryType **枚举**,
241
+ 原代码却拿它和 `[t.value for t in content_types]` 这个**字符串列表**比对——
242
+ 枚举永远不等于字符串,所以只要传了 content_types,命中就全被丢弃、返回空;
243
+ 而不传 content_types 时又走到 `MultimodalType(mtype)`,用枚举对象构造(应为
244
+ str),抛 ValueError。即「带过滤 = 静默漏检,不带过滤 = 直接崩溃」。
245
+ 现在两侧统一归一到 .value 再比较,类型名无法识别时按 TEXT 暴露而非丢结果。
246
+ """
247
+ if not self.index:
248
+ return []
249
+
250
+ results = self.index.search(query, top_k=limit * 2)
251
+
252
+ # 归一过滤集合:既接受 MultimodalType 枚举,也接受裸字符串值
253
+ allowed = None
254
+ if content_types:
255
+ allowed = {getattr(t, "value", t) for t in content_types}
256
+
257
+ filtered = []
258
+ for doc_id, score in results:
259
+ if self.storage:
260
+ entry = self.storage.get_memory(doc_id)
261
+ if entry:
262
+ mtype = getattr(entry, "memory_type", None)
263
+ # 兼容 MemoryType 枚举与裸字符串两种落库形态
264
+ type_value = getattr(mtype, "value", mtype)
265
+ if allowed is not None and type_value not in allowed:
266
+ continue
267
+ try:
268
+ ctype = MultimodalType(type_value)
269
+ except ValueError:
270
+ # 未知/缺失类型:按文本暴露,不让整条结果消失
271
+ ctype = MultimodalType.TEXT
272
+ content = MultimodalContent(
273
+ content_type=ctype,
274
+ data=entry.content,
275
+ text_representation=entry.content,
276
+ )
277
+ filtered.append((content, score))
278
+
279
+ return filtered[:limit]
280
+
281
+ def get_type_stats(self) -> Dict[str, int]:
282
+ """获取各类型统计"""
283
+ if not self.storage:
284
+ return self._type_stats
285
+
286
+ stats = self.storage.get_stats()
287
+ return stats.get("by_type", {})
288
+
289
+ @staticmethod
290
+ def compute_similarity(content1: MultimodalContent, content2: MultimodalContent) -> float:
291
+ """计算两个多模态内容的相似度"""
292
+ text1 = content1.text_representation.lower()
293
+ text2 = content2.text_representation.lower()
294
+
295
+ words1 = set(text1.split())
296
+ words2 = set(text2.split())
297
+
298
+ if not words1 or not words2:
299
+ return 0.0
300
+
301
+ intersection = words1 & words2
302
+ union = words1 | words2
303
+
304
+ return len(intersection) / len(union)
@@ -0,0 +1,186 @@
1
+ """
2
+ MindForge v6.0.0 功能 5:按结果学习的记忆
3
+
4
+ 不止记"发生过什么",还要记"哪段记忆带来了好结果":
5
+ - 成功的方法 → 记忆重要度上调,检索时优先
6
+ - 失败的方法 → 自动降权
7
+ - 可在记忆中标注 outcome 并给出解释
8
+
9
+ 实现:
10
+ - memories 表新增 outcome / outcome_note / outcome_at 三列(migration)
11
+ - apply_outcome(): 更新记忆的结果标记并调整 importance
12
+ - search_weighted(): 检索时按 outcome 加权(success 优先)
13
+ - outcome_stats(): 结果分布统计
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import time
19
+ from typing import Any, Dict, List, Optional
20
+
21
+ _OUTCOME_IMPORTANCE_MAP = {
22
+ "success": {"HIGH": 0, "MEDIUM": 1, "LOW": 1}, # 成功 → 上调
23
+ "partial": {"HIGH": 0, "MEDIUM": 0, "LOW": 0},
24
+ "failure": {"HIGH": 0, "MEDIUM": -1, "LOW": -1}, # 失败 → 下调
25
+ }
26
+
27
+ _IMPORTANCE_ORDER = ["LOW", "MEDIUM", "HIGH"]
28
+
29
+
30
+ def _shift_importance(importance: str, outcome: str) -> str:
31
+ """按结果调整重要度档位"""
32
+ imp = (importance or "MEDIUM").upper()
33
+ if imp not in _IMPORTANCE_ORDER:
34
+ imp = "MEDIUM"
35
+ shift = _OUTCOME_IMPORTANCE_MAP.get(outcome, {}).get(imp, 0)
36
+ idx = _IMPORTANCE_ORDER.index(imp)
37
+ idx = max(0, min(len(_IMPORTANCE_ORDER) - 1, idx + shift))
38
+ return _IMPORTANCE_ORDER[idx]
39
+
40
+
41
+ def _ensure_columns(conn) -> None:
42
+ """幂等添加 outcome 列"""
43
+ for col, ddl in (
44
+ ("outcome", "TEXT DEFAULT ''"),
45
+ ("outcome_note", "TEXT DEFAULT ''"),
46
+ ("outcome_at", "REAL DEFAULT 0"),
47
+ ):
48
+ try:
49
+ conn.execute(f"ALTER TABLE memories ADD COLUMN {col} {ddl}")
50
+ except Exception:
51
+ pass # 已存在
52
+
53
+
54
+ def _normalize_memory_id(memory_id: Any) -> tuple:
55
+ """接受 str 或 MemoryEntry 对象,返回 (id_or_None, error_or_None)。
56
+
57
+ v6.0.0 只接受字符串;但 mf.add() 返回的是 MemoryEntry 对象,
58
+ 把返回值直接传进来会在 len() 处抛 TypeError(对外表现为崩溃而非友好报错)。
59
+ v6.1.1 起统一收敛:对象取 .id,其余非法类型返回明确错误。
60
+ """
61
+ if memory_id is None:
62
+ return None, "记忆 ID 无效"
63
+ if not isinstance(memory_id, str):
64
+ memory_id = getattr(memory_id, "id", None)
65
+ if not isinstance(memory_id, str):
66
+ return None, "记忆 ID 必须是字符串或 add() 返回的 MemoryEntry 对象"
67
+ if not memory_id or len(memory_id) > 128:
68
+ return None, "记忆 ID 无效"
69
+ return memory_id, None
70
+
71
+
72
+ def _chunk_score(chunk: Any) -> float:
73
+ """读取召回得分。
74
+
75
+ RecallEngine 写入的字段名是 relevance_score(见 modules/recall.py),
76
+ v6.0.0 的 search_weighted 误读为 score,getattr 取不到就落回 0.0,
77
+ 导致按结果加权检索返回的 relevance 恒为 0.0。
78
+ """
79
+ for attr in ("relevance_score", "score", "similarity"):
80
+ val = getattr(chunk, attr, None)
81
+ if val is not None:
82
+ try:
83
+ return float(val)
84
+ except (TypeError, ValueError):
85
+ continue
86
+ if isinstance(chunk, dict):
87
+ for key in ("relevance_score", "score", "similarity"):
88
+ if chunk.get(key) is not None:
89
+ try:
90
+ return float(chunk[key])
91
+ except (TypeError, ValueError):
92
+ continue
93
+ return 0.0
94
+
95
+
96
+ def apply_outcome(storage, memory_id: str, outcome: str = "success",
97
+ note: str = "", actor: str = "",
98
+ session_id: str = "") -> Dict[str, Any]:
99
+ """给记忆标记结果并调整重要度。outcome: success/partial/failure"""
100
+ outcome = (outcome or "success").lower()
101
+ if outcome not in ("success", "partial", "failure"):
102
+ return {"success": False, "error": f"无效 outcome: {outcome}"}
103
+ memory_id, id_error = _normalize_memory_id(memory_id)
104
+ if id_error:
105
+ return {"success": False, "error": id_error}
106
+
107
+ conn = storage._get_conn()
108
+ _ensure_columns(conn)
109
+ row = conn.execute(
110
+ "SELECT importance, outcome FROM memories WHERE id = ?", (memory_id,)).fetchone()
111
+ if not row:
112
+ return {"success": False, "error": "记忆不存在"}
113
+ old_importance, old_outcome = row[0] or "MEDIUM", row[1] or ""
114
+ new_importance = _shift_importance(old_importance, outcome)
115
+
116
+ now = time.time()
117
+ conn.execute(
118
+ "UPDATE memories SET outcome = ?, outcome_note = ?, outcome_at = ?, "
119
+ "importance = ? WHERE id = ?",
120
+ (outcome, (note or "")[:500], now, new_importance, memory_id))
121
+ conn.commit()
122
+ storage._add_audit("outcome_learn", memory_id, actor or "", session_id or "",
123
+ "", {"outcome": outcome, "importance": new_importance,
124
+ "note": (note or "")[:200]})
125
+ return {"success": True, "memory_id": memory_id, "outcome": outcome,
126
+ "importance_before": old_importance, "importance_after": new_importance,
127
+ "outcome_at": now}
128
+
129
+
130
+ def outcome_stats(storage) -> Dict[str, Any]:
131
+ """结果标记分布统计"""
132
+ conn = storage._get_conn()
133
+ _ensure_columns(conn)
134
+ try:
135
+ rows = conn.execute(
136
+ "SELECT outcome, COUNT(*) FROM memories WHERE outcome != '' "
137
+ "GROUP BY outcome").fetchall()
138
+ except Exception:
139
+ rows = []
140
+ dist = {r[0]: r[1] for r in rows}
141
+ total = sum(dist.values())
142
+ return {
143
+ "total_marked": total,
144
+ "distribution": dist,
145
+ "success_rate": round(dist.get("success", 0) / total, 3) if total else 0.0,
146
+ }
147
+
148
+
149
+ def search_weighted(mf, query: str, max_results: int = 10,
150
+ outcome: str = "success",
151
+ min_relevance: float = 0.0) -> List[Dict[str, Any]]:
152
+ """按结果加权检索:先用正常检索召回,再把命中 outcome 的记忆提到前面。
153
+
154
+ 返回 [{memory_id, content_preview, outcome, importance, relevance}...]
155
+ """
156
+ from mindforge.modules.outcome_learning import _ensure_columns
157
+
158
+ results = mf.search(query=query, max_results=max(5, int(max_results) * 2),
159
+ min_relevance=float(min_relevance or 0.0))
160
+ chunks = getattr(results, "chunks", None) or (results if isinstance(results, list) else [])
161
+ out = []
162
+ for r in chunks:
163
+ mid = getattr(r, "memory_id", None) or getattr(r, "id", None)
164
+ if not mid:
165
+ continue
166
+ row = None
167
+ try:
168
+ conn = mf._storage._get_conn()
169
+ _ensure_columns(conn)
170
+ row = conn.execute(
171
+ "SELECT outcome, importance, content FROM memories WHERE id = ?",
172
+ (mid,)).fetchone()
173
+ except Exception:
174
+ pass
175
+ out.append({
176
+ "memory_id": mid,
177
+ "content_preview": (getattr(r, "content", "") or "")[:200],
178
+ "outcome": row[0] if row else "",
179
+ "importance": row[1] if row else "",
180
+ "relevance": round(_chunk_score(r), 4),
181
+ })
182
+
183
+ # 目标 outcome 的记忆前置
184
+ out.sort(key=lambda x: (0 if x["outcome"] == outcome else 1,
185
+ -x["relevance"]))
186
+ return out[:max(1, int(max_results))]