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,315 @@
1
+ """
2
+ MindForge v6.0.0 功能 8:记忆主动收尾
3
+
4
+ 记忆不再被动等你问,而是在正确时间主动说"你上次那件事还没办完"。
5
+
6
+ - create_follow_up(): 从记忆登记待跟进事项(due_at 到期时间)
7
+ - list_due(): 查询已到期未办结事项
8
+ - nag_due(): 主动催办并生成回执(v6.1.1)
9
+ - resolve_follow_up(): 办结
10
+ - list_follow_ups(): 列出(含已办结)
11
+ - 与 EventBus 联动:nag_due() 催办时 publish "memory.follow_up.due" 事件
12
+
13
+ 存储:follow_ups 表(独立于 memories,避免污染记忆表)
14
+ v6.1.1 为该表补 nag_count / last_nagged_at / receipt 三列(幂等 ALTER,旧库自动升级)。
15
+
16
+ 历史更正:本模块文档自 v6.0.0 起就写着"与 EventBus 联动:到期时 publish 事件",
17
+ 但 v6.0.0 全文没有任何 publish 调用,也没有可记录催办次数的字段——"记忆主动帮你
18
+ 收尾"实际只能被动查询。v6.1.1 起该声明由 nag_due() 真正实现。
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import time
24
+ import uuid
25
+ from typing import Any, Dict, List, Optional
26
+
27
+ _TABLE_SQL = """
28
+ CREATE TABLE IF NOT EXISTS follow_ups (
29
+ id TEXT PRIMARY KEY,
30
+ memory_id TEXT NOT NULL DEFAULT '',
31
+ note TEXT DEFAULT '',
32
+ due_at REAL DEFAULT 0,
33
+ status TEXT DEFAULT 'open',
34
+ created_at REAL DEFAULT 0,
35
+ resolved_at REAL DEFAULT 0,
36
+ actor TEXT DEFAULT ''
37
+ );
38
+ CREATE INDEX IF NOT EXISTS idx_follow_ups_due ON follow_ups(due_at);
39
+ CREATE INDEX IF NOT EXISTS idx_follow_ups_status ON follow_ups(status);
40
+ """
41
+
42
+ # v6.1.1:催办计数与回执。此前 follow_ups 只有 open/resolved 两态,
43
+ # 「已催了一次」无处可记,模块文档声称的到期事件也从无 publish 调用。
44
+ _NAG_COLUMNS = (
45
+ ("nag_count", "INTEGER DEFAULT 0"),
46
+ ("last_nagged_at", "REAL DEFAULT 0"),
47
+ ("receipt", "TEXT DEFAULT ''"),
48
+ )
49
+
50
+ _SELECT_COLS = ("id, memory_id, note, due_at, status, created_at, actor, "
51
+ "nag_count, last_nagged_at, receipt")
52
+ # v6.0.0 的 follow_ups 只有前 7 列。新增三列靠 _ensure_table() 懒加,而该函数
53
+ # 整体包在 except: pass 里;若迁移因任何原因未完成,直接 SELECT 新列会抛
54
+ # OperationalError。故查询列需按实际表结构自适应。
55
+ _SELECT_COLS_LEGACY = "id, memory_id, note, due_at, status, created_at, actor"
56
+
57
+
58
+ def _select_cols(conn) -> str:
59
+ """返回当前表可用的 SELECT 列清单。"""
60
+ try:
61
+ have = {r[1] for r in conn.execute("PRAGMA table_info(follow_ups)")}
62
+ except Exception:
63
+ return _SELECT_COLS_LEGACY
64
+ if {"nag_count", "last_nagged_at", "receipt"} <= have:
65
+ return _SELECT_COLS
66
+ return _SELECT_COLS_LEGACY
67
+
68
+
69
+ def _ensure_table(storage) -> None:
70
+ try:
71
+ conn = storage._get_conn()
72
+ conn.executescript(_TABLE_SQL)
73
+ for col, ddl in _NAG_COLUMNS:
74
+ try:
75
+ conn.execute(f"ALTER TABLE follow_ups ADD COLUMN {col} {ddl}")
76
+ except Exception:
77
+ pass # 已存在
78
+ conn.commit()
79
+ except Exception:
80
+ pass
81
+
82
+
83
+ def _normalize_memory_id(memory_id: Any) -> tuple:
84
+ """接受 str 或 add() 返回的 MemoryEntry,返回 (id, error)。
85
+
86
+ v6.0.0 直接 len(memory_id),传对象会抛 TypeError 而非友好报错。
87
+ """
88
+ if memory_id is None:
89
+ return None, "记忆 ID 无效"
90
+ if not isinstance(memory_id, str):
91
+ memory_id = getattr(memory_id, "id", None)
92
+ if not isinstance(memory_id, str):
93
+ return None, "记忆 ID 必须是字符串或 add() 返回的 MemoryEntry 对象"
94
+ if not memory_id or len(memory_id) > 128:
95
+ return None, "记忆 ID 无效"
96
+ return memory_id, None
97
+
98
+
99
+ def _publish_due_event(mf, payload: Dict[str, Any]) -> bool:
100
+ """发布 memory.follow_up.due 事件;失败不影响主流程。"""
101
+ try:
102
+ if getattr(mf, "_event_bus", None) is None:
103
+ from mindforge.modules.event_bus import EventBus
104
+ mf._event_bus = EventBus()
105
+ res = mf._event_bus.publish("memory.follow_up.due", payload)
106
+ return not (isinstance(res, dict) and res.get("skipped"))
107
+ except Exception:
108
+ return False
109
+
110
+
111
+ def create_follow_up(mf, memory_id: str, due_at: float = 0.0,
112
+ note: str = "", actor: str = "",
113
+ session_id: str = "") -> Dict[str, Any]:
114
+ """从记忆登记一条待跟进事项。due_at 缺省为 24 小时后。"""
115
+ memory_id, id_error = _normalize_memory_id(memory_id)
116
+ if id_error:
117
+ return {"success": False, "error": id_error}
118
+ # 确认记忆存在
119
+ entry = mf._storage.get_memory(memory_id, actor, session_id)
120
+ if entry is None:
121
+ return {"success": False, "error": "记忆不存在"}
122
+ _ensure_table(mf._storage)
123
+ now = time.time()
124
+ due = float(due_at or 0.0) or (now + 86400)
125
+ fu_id = f"fu_{uuid.uuid4().hex[:12]}"
126
+ conn = mf._storage._get_conn()
127
+ conn.execute(
128
+ "INSERT INTO follow_ups (id, memory_id, note, due_at, status, created_at, "
129
+ "resolved_at, actor, nag_count, last_nagged_at, receipt) "
130
+ "VALUES (?, ?, ?, ?, 'open', ?, 0, ?, 0, 0, '')",
131
+ (fu_id, memory_id, (note or "")[:500], due, now, actor or ""))
132
+ conn.commit()
133
+ mf._storage._add_audit(
134
+ "follow_up_create", memory_id, actor or "", session_id or "", "",
135
+ {"follow_up_id": fu_id, "due_at": due})
136
+ return {"success": True, "follow_up_id": fu_id, "memory_id": memory_id,
137
+ "due_at": due, "created_at": now, "note": note or "",
138
+ "nag_count": 0, "receipt": ""}
139
+
140
+
141
+ def list_due(mf, limit: int = 20) -> List[Dict[str, Any]]:
142
+ """查询已到期未办结的跟进事项"""
143
+ _ensure_table(mf._storage)
144
+ now = time.time()
145
+ conn = mf._storage._get_conn()
146
+ rows = conn.execute(
147
+ "SELECT " + _select_cols(conn) + " "
148
+ "FROM follow_ups WHERE status = 'open' AND due_at <= ? "
149
+ "ORDER BY due_at ASC LIMIT ?", (now, max(1, min(500, int(limit))))).fetchall()
150
+ return [_row_to_dict(r) for r in rows]
151
+
152
+
153
+ def nag_due(mf, interval_hours: float = 24.0, max_nags: int = 3,
154
+ actor: str = "memory", session_id: str = "") -> Dict[str, Any]:
155
+ """主动催办到期未办结的跟进事项,并生成回执(v6.1.1)。
156
+
157
+ 此前 follow_up 只能被动查询:模块文档写着「与 EventBus 联动:到期时
158
+ publish memory.follow_up.due」,但全文没有任何 publish 调用,表里也没有
159
+ 可记录「已催了几次」的字段,所以「记忆主动帮你收尾」无法演示。
160
+
161
+ - 已到期且距上次催办超过 interval_hours 的 open 事项 → nag_count + 1
162
+ - 生成人读回执文本(receipt),并审计 follow_up_nag
163
+ - 向 EventBus 发布 memory.follow_up.due;事件失败不影响催办落库
164
+ - 同一事项最多催 max_nags 次,避免无限骚扰
165
+
166
+ Args:
167
+ interval_hours: 两次催办之间的最小间隔(小时)。
168
+ max_nags: 单条事项的催办次数上限。
169
+
170
+ Returns:
171
+ {"nagged": [回执字典...], "skipped_recent": int, "skipped_maxed": int}
172
+ """
173
+ _ensure_table(mf._storage)
174
+ now = time.time()
175
+ interval = max(0.0, float(interval_hours)) * 3600.0
176
+ cap = max(1, int(max_nags))
177
+ conn = mf._storage._get_conn()
178
+ if _select_cols(conn) == _SELECT_COLS_LEGACY:
179
+ # 迁移未完成(例如只读库或表结构异常):明确报错,
180
+ # 不让下面的 UPDATE 抛裸的 OperationalError
181
+ return {"error": "follow_ups 表缺少催办字段,无法记录催办次数",
182
+ "nagged": [], "skipped_recent": 0, "skipped_maxed": 0}
183
+ rows = conn.execute(
184
+ "SELECT " + _select_cols(conn) + " "
185
+ "FROM follow_ups WHERE status = 'open' AND due_at <= ? "
186
+ "ORDER BY due_at ASC LIMIT 500", (now,)).fetchall()
187
+
188
+ nagged: List[Dict[str, Any]] = []
189
+ skipped_recent = 0
190
+ skipped_maxed = 0
191
+
192
+ for r in rows:
193
+ cur = _row_to_dict(r)
194
+ if int(cur["nag_count"]) >= cap:
195
+ skipped_maxed += 1
196
+ continue
197
+ if cur["nag_count"] and (now - float(cur["last_nagged_at"])) < interval:
198
+ skipped_recent += 1
199
+ continue
200
+
201
+ overdue_hours = (now - float(cur["due_at"])) / 3600.0
202
+ times = int(cur["nag_count"]) + 1
203
+ note = (cur["note"] or "跟进事项").strip() or "跟进事项"
204
+ receipt = _build_receipt(note, times, overdue_hours)
205
+
206
+ conn.execute(
207
+ "UPDATE follow_ups SET nag_count = ?, last_nagged_at = ?, receipt = ? "
208
+ "WHERE id = ? AND status = 'open'",
209
+ (times, now, receipt, cur["follow_up_id"]))
210
+ conn.commit()
211
+
212
+ mf._storage._add_audit(
213
+ "follow_up_nag", cur["memory_id"], actor, session_id, "",
214
+ {"follow_up_id": cur["follow_up_id"], "nag_count": times,
215
+ "overdue_hours": round(overdue_hours, 2)})
216
+
217
+ item = {
218
+ "follow_up_id": cur["follow_up_id"],
219
+ "memory_id": cur["memory_id"],
220
+ "note": note,
221
+ "receipt": receipt,
222
+ "nag_count": times,
223
+ "last_nagged_at": now,
224
+ "due_at": float(cur["due_at"]),
225
+ "overdue_hours": round(overdue_hours, 2),
226
+ "status": "open",
227
+ }
228
+ nagged.append(item)
229
+ # 事件仅为通知面,发布失败不回滚催办
230
+ _publish_due_event(mf, {
231
+ "follow_up_id": cur["follow_up_id"],
232
+ "memory_id": cur["memory_id"],
233
+ "receipt": receipt,
234
+ "nag_count": times,
235
+ "actor": actor,
236
+ "session_id": session_id,
237
+ })
238
+
239
+ return {"nagged": nagged, "skipped_recent": skipped_recent,
240
+ "skipped_maxed": skipped_maxed}
241
+
242
+
243
+ def _build_receipt(note: str, times: int, overdue_hours: float) -> str:
244
+ """把「还没回 + 已催几次 + 逾期多久」拼成一句可直接展示的回执。"""
245
+ if overdue_hours < 24:
246
+ span = "今天"
247
+ elif overdue_hours < 24 * 7:
248
+ span = "%d 天" % int(overdue_hours // 24)
249
+ else:
250
+ span = "%d 周" % int(overdue_hours // (24 * 7))
251
+ cn = {1: "一", 2: "两", 3: "三"}.get(times, str(times))
252
+ return "「%s」到期 %s 仍无结果,已催了%s次。" % (note, span, cn)
253
+
254
+
255
+ def resolve_follow_up(mf, follow_up_id: str, actor: str = "",
256
+ session_id: str = "") -> Dict[str, Any]:
257
+ """办结一条跟进事项"""
258
+ if not follow_up_id or len(follow_up_id) > 128:
259
+ return {"success": False, "error": "follow_up_id 无效"}
260
+ _ensure_table(mf._storage)
261
+ conn = mf._storage._get_conn()
262
+ row = conn.execute(
263
+ "SELECT id, memory_id FROM follow_ups WHERE id = ?", (follow_up_id,)).fetchone()
264
+ if not row:
265
+ return {"success": False, "error": "跟进事项不存在"}
266
+ now = time.time()
267
+ conn.execute(
268
+ "UPDATE follow_ups SET status = 'resolved', resolved_at = ?, actor = ? "
269
+ "WHERE id = ? AND status = 'open'",
270
+ (now, actor or "", follow_up_id))
271
+ conn.commit()
272
+ mf._storage._add_audit(
273
+ "follow_up_resolve", row[1] or "", actor or "", session_id or "", "",
274
+ {"follow_up_id": follow_up_id})
275
+ return {"success": True, "follow_up_id": follow_up_id,
276
+ "memory_id": row[1] or "", "resolved_at": now}
277
+
278
+
279
+ def list_follow_ups(mf, include_resolved: bool = False,
280
+ limit: int = 50) -> List[Dict[str, Any]]:
281
+ """列出跟进事项"""
282
+ _ensure_table(mf._storage)
283
+ conn = mf._storage._get_conn()
284
+ if include_resolved:
285
+ rows = conn.execute(
286
+ "SELECT " + _select_cols(conn) + " "
287
+ "FROM follow_ups ORDER BY due_at ASC LIMIT ?",
288
+ (max(1, min(500, int(limit))),)).fetchall()
289
+ else:
290
+ rows = conn.execute(
291
+ "SELECT " + _select_cols(conn) + " "
292
+ "FROM follow_ups WHERE status = 'open' ORDER BY due_at ASC LIMIT ?",
293
+ (max(1, min(500, int(limit))),)).fetchall()
294
+ return [_row_to_dict(r) for r in rows]
295
+
296
+
297
+ def _row_to_dict(r) -> Dict[str, Any]:
298
+ """把查询行转成字典。
299
+
300
+ 兼容两种列形状:v6.1.1 的 10 列,以及迁移尚未发生时的 v6.0.0 7 列
301
+ (见 _select_cols)。缺列时补默认值,保证调用方永远拿到全部键。
302
+ """
303
+ row = list(r) + [0, 0.0, ""] # v6.0.0 形状缺省补齐
304
+ return {
305
+ "follow_up_id": row[0],
306
+ "memory_id": row[1],
307
+ "note": row[2],
308
+ "due_at": row[3],
309
+ "status": row[4],
310
+ "created_at": row[5],
311
+ "actor": row[6],
312
+ "nag_count": int(row[7] or 0),
313
+ "last_nagged_at": float(row[8] or 0.0),
314
+ "receipt": row[9] or "",
315
+ }