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,374 @@
1
+ """
2
+ MindForge v6.0.0 功能 1:一键导入记忆(ChatGPT / Claude 对话记录)
3
+
4
+ 解决冷启动:新用户装上后第一分钟就能导入历史对话,AI 立刻"认识你"。
5
+
6
+ - parse_export(): 解析 ChatGPT mapping 树与 Claude accounts/conversations 变体
7
+ - detect_platform(): 自动嗅探平台类型
8
+ - import_chat_history(): 将解析结果写入记忆库(画像信号 HIGH 重要度)
9
+ - 噪音过滤:is_user_signal / is_noise,导入阶段过滤系统噪音与重复问候
10
+
11
+ 安全约束:
12
+ - MAX_EXPORT_SIZE = 200MB,超大导出直接拒绝(v6.1.2 起真正生效:CLI 在读取文件前
13
+ 用 export_size_error() 拦截,facade 对未解析的原始文本/字节同样拦截)
14
+ - 每轮消息最大 32KB,超出截断
15
+ - 导入量上限 MAX_IMPORT_MESSAGES = 20000 条,防止内存爆炸
16
+ - 审计动作 import_chat 全量记录
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import logging
23
+ import re
24
+ import time
25
+ from typing import Any, Dict, List, Optional, Tuple
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ # 导出文件大小上限(200MB)
30
+ MAX_EXPORT_SIZE = 200 * 1024 * 1024
31
+ # 单条消息文本上限(32KB)
32
+ MAX_MESSAGE_CHARS = 32 * 1024
33
+ # 单次导入消息条数上限
34
+ MAX_IMPORT_MESSAGES = 20000
35
+ # 单次导入生成的记忆上限
36
+ MAX_IMPORT_MEMORIES = 5000
37
+
38
+ # 平台标识
39
+ PLATFORM_CHATGPT = "chatgpt"
40
+ PLATFORM_CLAUDE = "claude"
41
+ PLATFORM_UNKNOWN = "unknown"
42
+
43
+ SUPPORTED_PLATFORMS = (PLATFORM_CHATGPT, PLATFORM_CLAUDE)
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # 平台嗅探
47
+ # ---------------------------------------------------------------------------
48
+
49
+
50
+ def detect_platform(data: Any) -> str:
51
+ """根据导出 JSON 的结构嗅探平台。
52
+
53
+ - ChatGPT: 顶层含 mapping 字段(conversation_id -> node 树),
54
+ 或含 conversations 且每个会话含 mapping
55
+ - Claude: 顶层含 accounts,每个账号含 conversations(列表式)
56
+ """
57
+ if isinstance(data, dict):
58
+ if isinstance(data.get("mapping"), dict):
59
+ return PLATFORM_CHATGPT
60
+ if isinstance(data.get("conversations"), list):
61
+ # 会话带 mapping 视为 ChatGPT 变体;带 chat_messages 视为 Claude 变体
62
+ convs = data["conversations"]
63
+ if convs and isinstance(convs[0], dict) and isinstance(
64
+ convs[0].get("chat_messages"), list):
65
+ return PLATFORM_CLAUDE
66
+ return PLATFORM_CHATGPT
67
+ if isinstance(data.get("accounts"), dict):
68
+ return PLATFORM_CLAUDE
69
+ return PLATFORM_UNKNOWN
70
+
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # 噪音与用户信号
74
+ # ---------------------------------------------------------------------------
75
+
76
+ # 需要过滤的系统噪音模式(导入阶段剔除,不写入记忆)
77
+ _NOISE_PATTERNS = [
78
+ re.compile(r"^系统提示[::]", re.IGNORECASE),
79
+ re.compile(r"^System prompt[::]", re.IGNORECASE),
80
+ re.compile(r"^你是一个", re.IGNORECASE),
81
+ re.compile(r"^You are (an|a) ", re.IGNORECASE),
82
+ re.compile(r"^请忽略以上", re.IGNORECASE),
83
+ re.compile(r"^Ignore (all )?(the )?above", re.IGNORECASE),
84
+ re.compile(r"^这是(一个)?测试", re.IGNORECASE),
85
+ re.compile(r"^This is a test", re.IGNORECASE),
86
+ re.compile(r"^测试", re.IGNORECASE), # 测试性消息直接过滤
87
+ re.compile(r"^(你好|您好|哈喽|嗨|hello|hi)[,,。.!!~~]?$", re.IGNORECASE),
88
+ re.compile(r"^好的?,?(我|明白了|收到|了解)", re.IGNORECASE),
89
+ re.compile(r"^(OK|Got it|Understood|Sure)[,.!]?$", re.IGNORECASE),
90
+ re.compile(r"^谢谢[,,.!!]?$", re.IGNORECASE),
91
+ re.compile(r"^Thank you[,.]?$", re.IGNORECASE),
92
+ re.compile(r"^再见[,,.!!]?$", re.IGNORECASE),
93
+ re.compile(r"^Bye[,.]?$", re.IGNORECASE),
94
+ ]
95
+
96
+ # 用户画像信号(第一人称事实陈述)——命中即写入记忆
97
+ _SIGNAL_PATTERNS = [
98
+ re.compile(r"(?:我|本人)(?:住在|居住于|生活(?:在|于))", re.IGNORECASE),
99
+ re.compile(r"(?:我|本人)(?:喜欢|热爱|偏爱|钟爱|超爱)", re.IGNORECASE),
100
+ re.compile(r"(?:我|本人)(?:讨厌|不喜欢|反感)", re.IGNORECASE),
101
+ re.compile(r"(?:我|本人)(?:从事|就职于|工作(?:在|于)|任职于|在).{0,20}(?:工作|上班)", re.IGNORECASE),
102
+ re.compile(r"(?:我|本人)(?:是|是一名|是个|是一位)", re.IGNORECASE),
103
+ re.compile(r"(?:我|本人)(?:家在|来自)", re.IGNORECASE),
104
+ re.compile(r"(?:我|本人)(?:养了|养着|有一只|有一条)", re.IGNORECASE),
105
+ re.compile(r"(?:我|本人)(?:结婚|已婚|有孩子|有两个孩子|有小孩)", re.IGNORECASE),
106
+ re.compile(r"(?:我|本人)(?:最近|刚|正在)(?:在学|学习|研究|准备)", re.IGNORECASE),
107
+ re.compile(r"(?:我|本人)(?:的(?:生日|名字|手机号|邮箱|地址)是)", re.IGNORECASE),
108
+ re.compile(r"(?:我|本人)(?:想去|计划去|打算去|希望去)", re.IGNORECASE),
109
+ re.compile(r"(?:我|本人)(?:的目标|的梦想|的愿望|的长期计划)是", re.IGNORECASE),
110
+ re.compile(r"(?:我|本人)(?:的)(?:职业|职位|岗位|角色)是", re.IGNORECASE),
111
+ ]
112
+
113
+
114
+ def is_noise(text: str) -> bool:
115
+ """判断是否系统噪音/无信息量内容(导入阶段过滤)"""
116
+ t = (text or "").strip()
117
+ if not t:
118
+ return True
119
+ if len(t) > 4000:
120
+ return False # 长文本大概率是有内容的
121
+ for pat in _NOISE_PATTERNS:
122
+ if pat.match(t):
123
+ return True
124
+ return False
125
+
126
+
127
+ def is_user_signal(text: str) -> bool:
128
+ """判断是否为用户画像信号(第一人称事实)"""
129
+ t = (text or "").strip()
130
+ if not t or len(t) > 1000:
131
+ return False
132
+ for pat in _SIGNAL_PATTERNS:
133
+ if pat.search(t):
134
+ return True
135
+ return False
136
+
137
+
138
+ # ---------------------------------------------------------------------------
139
+ # 导出解析
140
+ # ---------------------------------------------------------------------------
141
+
142
+
143
+ def _extract_text_from_part(part: Any) -> str:
144
+ """从 ChatGPT mapping node 的 message.content.parts 提取纯文本"""
145
+ if not isinstance(part, dict):
146
+ return ""
147
+ content = part.get("content")
148
+ if isinstance(content, str):
149
+ return content
150
+ if isinstance(content, dict):
151
+ parts = content.get("parts")
152
+ if isinstance(parts, list):
153
+ texts = []
154
+ for p in parts:
155
+ if isinstance(p, str):
156
+ texts.append(p)
157
+ elif isinstance(p, dict) and isinstance(p.get("text"), str):
158
+ texts.append(p["text"])
159
+ return "\n".join(texts)
160
+ return ""
161
+
162
+
163
+ def parse_chatgpt_export(data: Dict[str, Any]) -> List[Dict[str, Any]]:
164
+ """解析 ChatGPT 导出(mapping 树)为统一消息列表。
165
+
166
+ 返回 [{"role","text","ts"}],按会话时间排序。
167
+ """
168
+ messages: List[Dict[str, Any]] = []
169
+ mapping = data.get("mapping")
170
+ if not isinstance(mapping, dict):
171
+ return messages
172
+
173
+ for _node_id, node in mapping.items():
174
+ if not isinstance(node, dict):
175
+ continue
176
+ msg = node.get("message")
177
+ if not isinstance(msg, dict):
178
+ continue
179
+ role = msg.get("author", {}).get("role", "")
180
+ if role not in ("user", "assistant"):
181
+ continue
182
+ text = _extract_text_from_part(msg)
183
+ if not text.strip():
184
+ continue
185
+ ts = msg.get("create_time") or 0.0
186
+ try:
187
+ ts = float(ts)
188
+ except (TypeError, ValueError):
189
+ ts = 0.0
190
+ messages.append({"role": role, "text": text, "ts": ts})
191
+
192
+ messages.sort(key=lambda m: m["ts"])
193
+ return messages
194
+
195
+
196
+ def parse_claude_export(data: Dict[str, Any]) -> List[Dict[str, Any]]:
197
+ """解析 Claude 导出(accounts.conversations,列表式 chat_messages)。
198
+
199
+ 返回统一消息列表。
200
+ """
201
+ messages: List[Dict[str, Any]] = []
202
+ accounts = data.get("accounts")
203
+ if not isinstance(accounts, dict):
204
+ return messages
205
+ for _acct, acct_data in accounts.items():
206
+ if not isinstance(acct_data, dict):
207
+ continue
208
+ convs = acct_data.get("conversations")
209
+ # Claude 导出:conversations 可能是列表,也可能是 {会话id: 会话} 映射
210
+ if isinstance(convs, dict):
211
+ conv_iter = convs.values()
212
+ elif isinstance(convs, list):
213
+ conv_iter = convs
214
+ else:
215
+ continue
216
+ for conv in conv_iter:
217
+ if not isinstance(conv, dict):
218
+ continue
219
+ chat = conv.get("chat_messages")
220
+ if not isinstance(chat, list):
221
+ continue
222
+ for m in chat:
223
+ if not isinstance(m, dict):
224
+ continue
225
+ role = m.get("sender") or m.get("role") or ""
226
+ # Claude 导出用 human/assistant;统一为 user/assistant
227
+ role = {"human": "user", "assistant": "assistant"}.get(role, role)
228
+ if role not in ("user", "assistant"):
229
+ continue
230
+ text = m.get("text") or m.get("content") or ""
231
+ if not isinstance(text, str) or not text.strip():
232
+ continue
233
+ ts = m.get("timestamp") or m.get("created_at") or 0.0
234
+ try:
235
+ ts = float(ts)
236
+ except (TypeError, ValueError):
237
+ ts = 0.0
238
+ messages.append({"role": role, "text": text, "ts": ts})
239
+ messages.sort(key=lambda m: m["ts"])
240
+ return messages
241
+
242
+
243
+ def parse_export(data: Any) -> List[Dict[str, Any]]:
244
+ """自动嗅探平台并解析导出文件。"""
245
+ platform = detect_platform(data)
246
+ if platform == PLATFORM_CHATGPT:
247
+ return parse_chatgpt_export(data)
248
+ if platform == PLATFORM_CLAUDE:
249
+ return parse_claude_export(data)
250
+ return []
251
+
252
+
253
+ # ---------------------------------------------------------------------------
254
+ # 记忆构建与导入
255
+ # ---------------------------------------------------------------------------
256
+
257
+
258
+ def _build_memory_payload(messages: List[Dict[str, Any]],
259
+ platform: str) -> List[Dict[str, Any]]:
260
+ """从消息流构建记忆 payload 列表。
261
+
262
+ 规则:
263
+ - 用户画像信号(第一人称事实)→ HIGH 重要度 + user_preference 标签
264
+ - 普通用户陈述 → MEDIUM
265
+ - 助手消息不直接写入(避免噪音),但参与上下文(此处仅保留用户侧)
266
+ - 每条消息作为独立记忆,来源会话标记 source_session/agent
267
+ """
268
+ payloads: List[Dict[str, Any]] = []
269
+ for m in messages:
270
+ if m["role"] != "user":
271
+ continue
272
+ text = (m.get("text") or "").strip()[:MAX_MESSAGE_CHARS]
273
+ if not text or is_noise(text):
274
+ continue
275
+ ts = m.get("ts") or 0.0
276
+ if is_user_signal(text):
277
+ payloads.append({
278
+ "content": text,
279
+ "category": "preference",
280
+ "tags": ["user_preference", "profile"],
281
+ "importance": "HIGH",
282
+ "source_session": f"import:{platform}",
283
+ "source_agent": f"{platform}-import",
284
+ "created_at": ts or time.time(),
285
+ })
286
+ else:
287
+ payloads.append({
288
+ "content": text,
289
+ "category": "general",
290
+ "tags": ["imported"],
291
+ "importance": "MEDIUM",
292
+ "source_session": f"import:{platform}",
293
+ "source_agent": f"{platform}-import",
294
+ "created_at": ts or time.time(),
295
+ })
296
+ if len(payloads) >= MAX_IMPORT_MEMORIES:
297
+ break
298
+ return payloads
299
+
300
+
301
+ def export_size_error(size_bytes: int) -> Optional[str]:
302
+ """按 MAX_EXPORT_SIZE 校验导出体积,超限返回中文错误文案,否则 None。
303
+
304
+ v6.1.2 修复:模块文档自 v6.0.0 起就写着「MAX_EXPORT_SIZE = 200MB,超大导出
305
+ 直接拒绝」,常量也定义了,但全项目从未引用它——超大文件照样被 `json.load`
306
+ 整份读进内存。这里把承诺变成真正的护栏,由调用方在**读取文件之前**用
307
+ `os.path.getsize()` 传入字节数(此时还没解析,拦截才有意义)。
308
+ """
309
+ if size_bytes and size_bytes > MAX_EXPORT_SIZE:
310
+ return (
311
+ "导出文件 %s MB 超过上限 %s MB(MAX_EXPORT_SIZE),已拒绝解析;"
312
+ "请先拆分导出文件或只导入近期片段"
313
+ % (size_bytes // (1024 * 1024), MAX_EXPORT_SIZE // (1024 * 1024))
314
+ )
315
+ return None
316
+
317
+
318
+ def import_chat_history(mf, data: Any, dry_run: bool = False,
319
+ actor: str = "", session_id: str = "") -> Dict[str, Any]:
320
+ """将导出数据导入记忆库(facade 层调用)。
321
+
322
+ 参数:
323
+ mf: MindForge facade 实例
324
+ data: 解析后的导出 JSON
325
+ dry_run: 试运行(只统计不写入)
326
+ actor/session_id: 审计身份
327
+
328
+ 返回统计 dict。
329
+ """
330
+ # 尚未解析的原始文本/字节:体积护栏在这里仍然有意义(v6.1.2)
331
+ if isinstance(data, (str, bytes)) and len(data) > MAX_EXPORT_SIZE:
332
+ return {"success": False, "error": export_size_error(len(data))}
333
+
334
+ platform = detect_platform(data)
335
+ messages = parse_export(data)
336
+ if not messages:
337
+ return {"success": False, "error": "未识别到有效对话(支持 ChatGPT/Claude 导出)"}
338
+
339
+ payloads = _build_memory_payload(messages, platform)
340
+ stats = {
341
+ "success": True,
342
+ "platform": platform,
343
+ "messages_parsed": len(messages),
344
+ "memories_candidate": len(payloads),
345
+ "imported": 0,
346
+ # 命名说明(v6.1.2 校正):本字段是「解析出的消息数 − 实际生成的候选记忆数」,
347
+ # 除系统噪音与重复问候外,也包含按策略不单独成条的助手消息与超长截断;
348
+ # 它是「未成为记忆的消息数」,不是严格的「噪音数」。保留键名以兼容既有
349
+ # CLI/REST 输出与回归测试。
350
+ "skipped_noise": max(0, len(messages) - len(payloads)),
351
+ "dry_run": bool(dry_run),
352
+ }
353
+ if dry_run:
354
+ return stats
355
+
356
+ imported = 0
357
+ for p in payloads:
358
+ try:
359
+ mf.add(
360
+ content=p["content"],
361
+ category=p["category"],
362
+ tags=p["tags"],
363
+ importance=p["importance"],
364
+ source_session=p["source_session"],
365
+ source_agent=p["source_agent"],
366
+ )
367
+ imported += 1
368
+ except Exception as e: # 单条失败不中断整体导入
369
+ logger.warning("导入单条记忆失败: %s", e)
370
+ stats["imported"] = imported
371
+ mf._storage._add_audit(
372
+ "import_chat", "", actor or "", session_id or "", "",
373
+ {"platform": platform, "parsed": len(messages), "imported": imported})
374
+ return stats
@@ -0,0 +1,196 @@
1
+ """
2
+ MindForge v5.0 记忆整合器
3
+ 记忆摘要、时间线、关联整合
4
+ """
5
+
6
+ import logging
7
+ import time
8
+ from dataclasses import dataclass
9
+ from typing import List, Dict, Optional
10
+ from collections import defaultdict
11
+
12
+ from mindforge.core.storage import StorageEngine, MemoryEntry
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class MemorySummary:
19
+ """记忆摘要"""
20
+ summary: str
21
+ key_points: List[str]
22
+ categories: Dict[str, int]
23
+ time_span: str
24
+ total_memories: int
25
+
26
+
27
+ @dataclass
28
+ class MemoryTimeline:
29
+ """记忆时间线"""
30
+ entries: List[MemoryEntry]
31
+ time_periods: Dict[str, List[MemoryEntry]]
32
+
33
+
34
+ class MemoryIntegrator:
35
+ """记忆整合器"""
36
+
37
+ def __init__(self, storage: StorageEngine):
38
+ self.storage = storage
39
+
40
+ def generate_summary(self,
41
+ category: Optional[str] = None,
42
+ days: int = 30,
43
+ max_points: int = 10) -> MemorySummary:
44
+ """生成记忆摘要"""
45
+ entries = self.storage.list_memories(category=category, limit=500)
46
+
47
+ now = time.time()
48
+ cutoff = now - days * 86400
49
+
50
+ recent = [e for e in entries if e.created_at >= cutoff]
51
+
52
+ categories = defaultdict(int)
53
+ for e in recent:
54
+ categories[e.category] += 1
55
+
56
+ sorted_entries = sorted(recent, key=lambda x: x.importance.to_int(), reverse=True)
57
+
58
+ key_points = []
59
+ for entry in sorted_entries[:max_points]:
60
+ content = entry.content
61
+ if entry.encrypted:
62
+ try:
63
+ content = self.storage.decrypt_content(entry)
64
+ except (ValueError, TypeError) as _dec_err:
65
+ logger.warning("整合记忆 %s 解密失败,跳过该条: %s",
66
+ entry.id, _dec_err)
67
+ preview = content[:80] + "..." if len(content) > 80 else content
68
+ key_points.append(preview)
69
+
70
+ if recent:
71
+ oldest = min(e.created_at for e in recent)
72
+ newest = max(e.created_at for e in recent)
73
+ time_span = f"{self._format_date(oldest)} - {self._format_date(newest)}"
74
+ else:
75
+ time_span = "无数据"
76
+
77
+ summary = f"过去 {days} 天共产生 {len(recent)} 条记忆," \
78
+ f"涵盖 {len(categories)} 个分类。"
79
+
80
+ if categories:
81
+ top_cat = max(categories.items(), key=lambda x: x[1])
82
+ summary += f"其中 {top_cat[0]} 类最多,有 {top_cat[1]} 条。"
83
+
84
+ return MemorySummary(
85
+ summary=summary,
86
+ key_points=key_points,
87
+ categories=dict(categories),
88
+ time_span=time_span,
89
+ total_memories=len(recent),
90
+ )
91
+
92
+ def generate_timeline(self,
93
+ category: Optional[str] = None,
94
+ limit: int = 100) -> MemoryTimeline:
95
+ """生成时间线"""
96
+ entries = self.storage.list_memories(category=category, limit=limit)
97
+ entries.sort(key=lambda x: x.created_at, reverse=True)
98
+
99
+ periods = defaultdict(list)
100
+ for entry in entries:
101
+ date_str = self._format_date(entry.created_at)
102
+ periods[date_str].append(entry)
103
+
104
+ return MemoryTimeline(
105
+ entries=entries,
106
+ time_periods=dict(periods),
107
+ )
108
+
109
+ def find_related_memories(self, memory_id: str,
110
+ max_related: int = 5) -> List[MemoryEntry]:
111
+ """查找相关记忆"""
112
+ target = self.storage.get_memory(memory_id)
113
+ if not target:
114
+ return []
115
+
116
+ target_content = target.content
117
+ if target.encrypted:
118
+ try:
119
+ target_content = self.storage.decrypt_content(target)
120
+ except (ValueError, TypeError) as _dec_err:
121
+ logger.warning("整合目标记忆 %s 解密失败,跳过: %s",
122
+ target.id, _dec_err)
123
+
124
+ target_words = set(target_content.lower().split())
125
+
126
+ all_entries = self.storage.list_memories(limit=200)
127
+ scored = []
128
+
129
+ for entry in all_entries:
130
+ if entry.id == memory_id:
131
+ continue
132
+
133
+ content = entry.content
134
+ if entry.encrypted:
135
+ try:
136
+ content = self.storage.decrypt_content(entry)
137
+ except (ValueError, TypeError) as _dec_err:
138
+ logger.warning("整合记忆 %s 解密失败,跳过该条: %s",
139
+ entry.id, _dec_err)
140
+
141
+ entry_words = set(content.lower().split())
142
+ common = target_words & entry_words
143
+ score = len(common) / max(len(target_words), 1)
144
+
145
+ if entry.category == target.category:
146
+ score += 0.2
147
+
148
+ if score > 0.05:
149
+ scored.append((entry, score))
150
+
151
+ scored.sort(key=lambda x: x[1], reverse=True)
152
+ return [e for e, _ in scored[:max_related]]
153
+
154
+ def consolidate_daily(self, date_str: Optional[str] = None) -> Dict:
155
+ """每日记忆整合"""
156
+ if not date_str:
157
+ date_str = self._format_date(time.time())
158
+
159
+ entries = self.storage.list_memories(limit=500)
160
+ day_entries = [
161
+ e for e in entries
162
+ if self._format_date(e.created_at) == date_str
163
+ ]
164
+
165
+ categories = defaultdict(int)
166
+ tags = defaultdict(int)
167
+ importance_dist = defaultdict(int)
168
+
169
+ for entry in day_entries:
170
+ categories[entry.category] += 1
171
+ for tag in entry.tags:
172
+ tags[tag] += 1
173
+ importance_dist[entry.importance.value] += 1
174
+
175
+ return {
176
+ "date": date_str,
177
+ "total_memories": len(day_entries),
178
+ "by_category": dict(categories),
179
+ "top_tags": sorted(tags.items(), key=lambda x: x[1], reverse=True)[:10],
180
+ "by_importance": dict(importance_dist),
181
+ }
182
+
183
+ def get_memory_clusters(self, n_clusters: int = 5) -> Dict[str, List[str]]:
184
+ """获取记忆聚类(简易版)"""
185
+ entries = self.storage.list_memories(limit=200)
186
+ clusters: Dict[str, List[str]] = defaultdict(list)
187
+
188
+ for entry in entries:
189
+ clusters[entry.category].append(entry.id)
190
+
191
+ return dict(clusters)
192
+
193
+ def _format_date(self, timestamp: float) -> str:
194
+ from datetime import datetime, timezone
195
+ dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
196
+ return dt.strftime("%Y-%m-%d")