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,363 @@
1
+ """
2
+ MindForge v5.0 索引引擎
3
+ 支持 TF-IDF、向量索引、FTS5 全文检索
4
+ """
5
+
6
+ import math
7
+ import os
8
+ import re
9
+ import sqlite3
10
+ from collections import Counter, defaultdict
11
+ from typing import List, Dict, Tuple, Optional
12
+ from dataclasses import dataclass
13
+
14
+
15
+ @dataclass
16
+ class IndexedDocument:
17
+ """索引文档"""
18
+ doc_id: str
19
+ text: str
20
+ metadata: Dict
21
+ vector: List[float]
22
+
23
+
24
+ class TFIDFVectorizer:
25
+ """TF-IDF 向量化器"""
26
+
27
+ def __init__(self):
28
+ self.vocab: Dict[str, int] = {}
29
+ self.idf: Dict[str, float] = {}
30
+ self.doc_count: int = 0
31
+
32
+ def _tokenize(self, text: str) -> List[str]:
33
+ text = text.lower()
34
+ tokens = []
35
+ for segment in re.findall(r'[a-z0-9]+|[\u4e00-\u9fff]+', text):
36
+ if re.match(r'[\u4e00-\u9fff]', segment):
37
+ if len(segment) <= 2:
38
+ tokens.append(segment)
39
+ else:
40
+ for i in range(len(segment) - 1):
41
+ tokens.append(segment[i:i+2])
42
+ elif len(segment) > 1:
43
+ tokens.append(segment)
44
+ return tokens
45
+
46
+ def fit(self, documents: List[str]):
47
+ self.doc_count = len(documents)
48
+ df = defaultdict(int)
49
+
50
+ all_tokens = set()
51
+ for doc in documents:
52
+ tokens = set(self._tokenize(doc))
53
+ for token in tokens:
54
+ df[token] += 1
55
+ all_tokens.add(token)
56
+
57
+ self.vocab = {word: idx for idx, word in enumerate(sorted(all_tokens))}
58
+
59
+ for word, idx in self.vocab.items():
60
+ self.idf[word] = math.log((self.doc_count + 1) / (df[word] + 1)) + 1
61
+
62
+ def transform(self, text: str) -> Dict[int, float]:
63
+ tokens = self._tokenize(text)
64
+ if not tokens:
65
+ return {}
66
+
67
+ tf = Counter(tokens)
68
+ max_tf = max(tf.values())
69
+
70
+ vector = {}
71
+ for token, count in tf.items():
72
+ if token in self.vocab:
73
+ idx = self.vocab[token]
74
+ tf_norm = 0.5 + 0.5 * count / max_tf
75
+ vector[idx] = tf_norm * self.idf.get(token, 0)
76
+
77
+ norm = math.sqrt(sum(v * v for v in vector.values()))
78
+ if norm > 0:
79
+ vector = {k: v / norm for k, v in vector.items()}
80
+
81
+ return vector
82
+
83
+ def cosine_similarity(self, v1: Dict[int, float], v2: Dict[int, float]) -> float:
84
+ common = set(v1.keys()) & set(v2.keys())
85
+ dot = sum(v1[k] * v2[k] for k in common)
86
+ return dot
87
+
88
+
89
+ class VectorIndex:
90
+ """向量索引
91
+
92
+ v5.6.3 性能修复:向量以稀疏字典存储。TF-IDF 中文 bigram 词表可达上万维,
93
+ 旧实现把每篇文档展开成词表长度的稠密 list,单次搜索退化为 O(N*V) 全表
94
+ 浮点点积(N=文档数、V=词表大小,二者随语料同时增长),3k 条记忆的混合
95
+ 搜索因此达到 ~0.9s。现在:
96
+ - 文档/查询向量一律稀疏化(只保留非零维,单条中文记忆通常仅几十个
97
+ 非零 bigram 权重);
98
+ - 维护维度 -> {doc_id: 权重} 的倒排链 _postings,查询只累计共享非零
99
+ 维的候选文档,点积复杂度降到 O(候选数 * 查询非零维);
100
+ - add/remove 同步维护倒排链,覆盖写入先清旧链。
101
+ 为兼容外部直接使用本类,仍接受稠密 list/tuple 输入。
102
+ """
103
+
104
+ def __init__(self, dim: int = 384):
105
+ self.dim = dim
106
+ self.vectors: Dict[str, Dict[int, float]] = {}
107
+ self.metadata: Dict[str, Dict] = {}
108
+ self._norms: Dict[str, float] = {} # 预计算 L2 范数缓存
109
+ self._postings: Dict[int, Dict[str, float]] = {} # 倒排链
110
+
111
+ @staticmethod
112
+ def _as_sparse(vector) -> Dict[int, float]:
113
+ """稀疏 dict(dict 输入)或稠密 list/tuple 统一转成 {dim: weight}"""
114
+ if isinstance(vector, dict):
115
+ return {int(k): float(v) for k, v in vector.items() if v != 0.0}
116
+ return {i: float(v) for i, v in enumerate(vector) if v != 0.0}
117
+
118
+ def _remove_from_postings(self, doc_id: str, sparse: Dict[int, float]):
119
+ for idx in sparse:
120
+ chain = self._postings.get(idx)
121
+ if chain is not None:
122
+ chain.pop(doc_id, None)
123
+ if not chain:
124
+ del self._postings[idx]
125
+
126
+ def add(self, doc_id: str, vector, metadata: Optional[Dict] = None):
127
+ sparse = self._as_sparse(vector)
128
+ if doc_id in self.vectors:
129
+ # 覆盖旧向量:先清理旧倒排链,避免过期权重残留
130
+ self._remove_from_postings(doc_id, self.vectors[doc_id])
131
+ self.vectors[doc_id] = sparse
132
+ if metadata:
133
+ self.metadata[doc_id] = metadata
134
+ self._norms[doc_id] = math.sqrt(sum(v * v for v in sparse.values()))
135
+ for idx, val in sparse.items():
136
+ self._postings.setdefault(idx, {})[doc_id] = val
137
+
138
+ def remove(self, doc_id: str):
139
+ sparse = self.vectors.pop(doc_id, None)
140
+ self.metadata.pop(doc_id, None)
141
+ self._norms.pop(doc_id, None)
142
+ if sparse is not None:
143
+ self._remove_from_postings(doc_id, sparse)
144
+
145
+ def search(self, query_vector, top_k: int = 10) -> List[Tuple[str, float]]:
146
+ if not self.vectors:
147
+ return []
148
+
149
+ q = self._as_sparse(query_vector)
150
+ query_norm = math.sqrt(sum(v * v for v in q.values()))
151
+ if query_norm == 0:
152
+ return [(doc_id, 0.0) for doc_id in list(self.vectors.keys())[:top_k]]
153
+
154
+ # 倒排链累加点积:只访问与查询共享非零维度的候选文档
155
+ dots: Dict[str, float] = {}
156
+ for idx, qval in q.items():
157
+ for doc_id, dval in self._postings.get(idx, {}).items():
158
+ dots[doc_id] = dots.get(doc_id, 0.0) + qval * dval
159
+
160
+ scores = [(doc_id, dot / (query_norm * self._norms[doc_id]))
161
+ for doc_id, dot in dots.items()
162
+ if self._norms.get(doc_id, 0.0) > 0]
163
+ scores.sort(key=lambda x: x[1], reverse=True)
164
+ return scores[:top_k]
165
+
166
+ def _cosine(self, v1, v2) -> float:
167
+ """保留原始接口(向后兼容),稀疏/稠密输入均支持"""
168
+ s1 = self._as_sparse(v1)
169
+ s2 = self._as_sparse(v2)
170
+ n1 = math.sqrt(sum(a * a for a in s1.values()))
171
+ n2 = math.sqrt(sum(a * a for a in s2.values()))
172
+ if n1 == 0 or n2 == 0:
173
+ return 0.0
174
+ dot = sum(val * s2.get(k, 0.0) for k, val in s1.items())
175
+ return dot / (n1 * n2)
176
+
177
+
178
+ class IndexEngine:
179
+ """索引引擎
180
+
181
+ v5.6.5 P1 #5:内存结构(``_doc_texts`` / 稀疏向量 / 倒排链)设置
182
+ LRU 上限,默认 ``DEFAULT_MAX_DOCS`` 条,可由构造参数或环境变量
183
+ ``MINDFORGE_INDEX_MAX_DOCS`` 覆盖(设为 0/负数表示不限制)。超限时
184
+ 最久未写入的文档从该进程内加速器淘汰。被淘汰文档不会丢失可检索性:
185
+ QueryEngine 多路召回同时走 SQLite FTS5 与 fuzzy(直接扫持久层),
186
+ 按 id 取最高分合并,因此 TF-IDF 加速器有界不影响最终召回。
187
+ """
188
+
189
+ DEFAULT_MAX_DOCS = 100_000
190
+
191
+ def __init__(self, db_path: str = "./data/memory.db",
192
+ max_docs: Optional[int] = None):
193
+ self.db_path = db_path
194
+ self.vectorizer = TFIDFVectorizer()
195
+ self.vector_index = VectorIndex()
196
+ self._fitted = False
197
+ self._hydrated = False
198
+ self._doc_texts: Dict[str, str] = {}
199
+ if max_docs is None:
200
+ try:
201
+ max_docs = int(os.environ.get("MINDFORGE_INDEX_MAX_DOCS",
202
+ self.DEFAULT_MAX_DOCS))
203
+ except (TypeError, ValueError):
204
+ max_docs = self.DEFAULT_MAX_DOCS
205
+ # None 语义保留:显式 <=0 表示不限制
206
+ self.max_docs = max_docs if max_docs and max_docs > 0 else None
207
+ self.evicted_count = 0
208
+
209
+ @property
210
+ def needs_hydration(self) -> bool:
211
+ """是否需要从持久层水合(v5.2.8 新增)
212
+
213
+ TF-IDF 向量索引是进程内存结构,CLI 等短生命周期进程启动时为空,
214
+ 必须先水合才能搜索到历史记忆。
215
+ """
216
+ return not self._hydrated
217
+
218
+ def hydrate(self, documents: Dict[str, str]) -> int:
219
+ """从持久层水合文档(v5.2.8 新增:修复跨进程搜索失效)
220
+
221
+ Args:
222
+ documents: {memory_id: content} 映射(通常来自
223
+ StorageEngine.get_indexable_documents())
224
+
225
+ Returns:
226
+ 水合的文档数量
227
+ """
228
+ if self._hydrated:
229
+ return 0
230
+ self._doc_texts.update(documents)
231
+ self._evict_if_needed()
232
+ # 强制下次搜索时重新 fit,确保词表覆盖保留文档(被淘汰文档仍可经
233
+ # FTS5/fuzzy 召回,见类文档说明)
234
+ self._fitted = False
235
+ self._hydrated = True
236
+ return len(documents)
237
+
238
+ def _evict_if_needed(self) -> None:
239
+ """超过内存上限时,按插入顺序(近似 LRU)淘汰最旧文档,
240
+ 同步从 _doc_texts、稀疏向量、倒排链三处移除,保证三者一致。"""
241
+ if self.max_docs is None:
242
+ return
243
+ while len(self._doc_texts) > self.max_docs:
244
+ oldest = next(iter(self._doc_texts))
245
+ del self._doc_texts[oldest]
246
+ self.vector_index.remove(oldest)
247
+ self.evicted_count += 1
248
+
249
+ def index_memory(self, doc_id: str, text: str, metadata: Optional[Dict] = None):
250
+ """索引记忆"""
251
+ self._doc_texts[doc_id] = text
252
+
253
+ if not self._fitted and len(self._doc_texts) >= 5:
254
+ self._fit_vectorizer()
255
+
256
+ if self._fitted:
257
+ # v5.6.3 性能修复:直接存稀疏向量({dim: weight}),不再展开为
258
+ # 词表长度的稠密 list
259
+ vector_dict = self.vectorizer.transform(text)
260
+ self.vector_index.add(doc_id, vector_dict, metadata)
261
+
262
+ # v5.6.5 P1 #5:写入后做 LRU 淘汰(覆盖写不增加条目数,故在 add 之后)
263
+ self._evict_if_needed()
264
+
265
+ def remove_memory(self, doc_id: str):
266
+ """移除索引"""
267
+ self._doc_texts.pop(doc_id, None)
268
+ self.vector_index.remove(doc_id)
269
+
270
+ def search(self, query: str, top_k: int = 10) -> List[Tuple[str, float]]:
271
+ """搜索"""
272
+ if not self._fitted and self._doc_texts:
273
+ self._fit_vectorizer()
274
+
275
+ if not self._fitted:
276
+ return self._keyword_search(query, top_k)
277
+
278
+ # v5.6.3 性能修复:查询向量同样保持稀疏,走倒排链检索
279
+ query_vec_dict = self.vectorizer.transform(query)
280
+ return self.vector_index.search(query_vec_dict, top_k)
281
+
282
+ def _keyword_search(self, query: str, top_k: int) -> List[Tuple[str, float]]:
283
+ """关键词搜索(降级方案)"""
284
+ query_lower = query.lower()
285
+ scores = []
286
+
287
+ for doc_id, text in self._doc_texts.items():
288
+ if not text:
289
+ continue # v6.1.5 Bug-2 fix: 空文本除零守卫
290
+ text_lower = text.lower()
291
+ score = 0
292
+ for word in query_lower.split():
293
+ if word in text_lower:
294
+ score += text_lower.count(word)
295
+ if score > 0:
296
+ scores.append((doc_id, score / len(text) * 1000))
297
+
298
+ scores.sort(key=lambda x: x[1], reverse=True)
299
+ return scores[:top_k]
300
+
301
+ def _fit_vectorizer(self):
302
+ texts = list(self._doc_texts.values())
303
+ self.vectorizer.fit(texts)
304
+
305
+ for doc_id, text in self._doc_texts.items():
306
+ vector_dict = self.vectorizer.transform(text)
307
+ self.vector_index.add(doc_id, vector_dict)
308
+
309
+ self._fitted = True
310
+
311
+ @staticmethod
312
+ def _escape_fts5_query(query: str) -> str:
313
+ """转义 FTS5 查询特殊字符(v5.4.7 新增,v5.7.1 优化多词召回)
314
+
315
+ FTS5 MATCH 语法中 + - * ( ) " : 等是特殊字符,
316
+ 直接传入会导致 OperationalError。用双引号包裹为短语查询,
317
+ 同时转义内部的双引号。
318
+
319
+ v5.7.1:按空白分词后用 OR 连接各短语。英文/数字多词查询
320
+ (如 "search bug")可同时召回含任一短语的文档;中文无空格
321
+ 仍为单短语查询,行为不变。
322
+ """
323
+ q = query.strip()
324
+ if not q:
325
+ return q
326
+ tokens = [t for t in q.split() if t]
327
+ # 单 token(含中文无空格)保持原短语行为
328
+ if len(tokens) <= 1:
329
+ return '"' + q.replace('"', '""') + '"'
330
+ # 多 token:各自转义为短语后 OR 连接
331
+ escaped = ['"' + t.replace('"', '""') + '"' for t in tokens]
332
+ return " OR ".join(escaped)
333
+
334
+ def fts_search(self, conn: sqlite3.Connection, query: str,
335
+ top_k: int = 10) -> List[Tuple[str, float]]:
336
+ """FTS5 全文搜索(v5.4.7 修复:特殊字符转义)"""
337
+ try:
338
+ escaped = self._escape_fts5_query(query)
339
+ if not escaped:
340
+ return []
341
+ rows = conn.execute("""
342
+ SELECT m.id, bm25(memory_fts) as score
343
+ FROM memory_fts
344
+ JOIN memories m ON memory_fts.rowid = m.rowid
345
+ WHERE memory_fts MATCH ?
346
+ AND m.category != 'trash'
347
+ ORDER BY score
348
+ LIMIT ?
349
+ """, (escaped, top_k)).fetchall()
350
+ # v5.5.2 fix: clamp bm25 score to prevent math.exp overflow.
351
+ # bm25 returns negative values (lower = more relevant); a large
352
+ # positive value is anomalous but would cause OverflowError.
353
+ results = []
354
+ for row in rows:
355
+ raw = row[1]
356
+ if raw is None:
357
+ continue
358
+ clamped = max(-50.0, min(50.0, float(raw)))
359
+ score = 1.0 / (1.0 + math.exp(clamped))
360
+ results.append((row[0], score))
361
+ return results
362
+ except sqlite3.OperationalError:
363
+ return []