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,275 @@
1
+ """
2
+ MindForge v5.0 人格化引擎
3
+ 学习用户偏好、语言风格、思维模式
4
+ """
5
+
6
+ import json
7
+ import logging
8
+ import time
9
+ import re
10
+ import sqlite3 # v5.4.7 修复 C-4:异常处理中引用 sqlite3.OperationalError 需要此导入
11
+ from dataclasses import dataclass, field
12
+ from typing import Dict, List, Optional, Any, Tuple
13
+
14
+ from mindforge.core.storage import StorageEngine
15
+
16
+ # v6.1.2 修复:与上面 C-4 同源——第 81 / 270 行的画像加载与持久化降级分支引用
17
+ # `logger.warning`,但模块从未定义 logger,降级路径本身抛 NameError。
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ @dataclass
22
+ class UserProfile:
23
+ """用户画像"""
24
+ user_id: str = "default"
25
+ preferences: Dict[str, float] = field(default_factory=dict)
26
+ language_style: Dict[str, Any] = field(default_factory=dict)
27
+ topics_of_interest: Dict[str, float] = field(default_factory=dict)
28
+ technical_level: Dict[str, float] = field(default_factory=dict)
29
+ communication_patterns: Dict[str, Any] = field(default_factory=dict)
30
+ learning_style: Dict[str, float] = field(default_factory=dict)
31
+ interaction_history: List[Dict] = field(default_factory=list)
32
+ last_updated: float = 0.0
33
+ total_interactions: int = 0
34
+
35
+ def to_dict(self) -> dict:
36
+ return {
37
+ "user_id": self.user_id,
38
+ "preferences": self.preferences,
39
+ "language_style": self.language_style,
40
+ "topics_of_interest": self.topics_of_interest,
41
+ "technical_level": self.technical_level,
42
+ "communication_patterns": self.communication_patterns,
43
+ "learning_style": self.learning_style,
44
+ "total_interactions": self.total_interactions,
45
+ "last_updated": self.last_updated,
46
+ }
47
+
48
+ @classmethod
49
+ def from_dict(cls, data: dict) -> "UserProfile":
50
+ return cls(
51
+ user_id=data.get("user_id", "default"),
52
+ preferences=data.get("preferences", {}),
53
+ language_style=data.get("language_style", {}),
54
+ topics_of_interest=data.get("topics_of_interest", {}),
55
+ technical_level=data.get("technical_level", {}),
56
+ communication_patterns=data.get("communication_patterns", {}),
57
+ learning_style=data.get("learning_style", {}),
58
+ total_interactions=data.get("total_interactions", 0),
59
+ last_updated=data.get("last_updated", 0),
60
+ )
61
+
62
+
63
+ class PersonalityEngine:
64
+ """人格化引擎"""
65
+
66
+ def __init__(self, storage: StorageEngine):
67
+ self.storage = storage
68
+ self.profiles: Dict[str, UserProfile] = {}
69
+ self._load_profiles()
70
+
71
+ def _load_profiles(self):
72
+ """加载用户画像"""
73
+ try:
74
+ all_memories = self.storage.list_memories(category="user_profile", limit=100)
75
+ for entry in all_memories:
76
+ try:
77
+ # P3 #29 修复:使用安全 JSON 解析,防深度嵌套栈溢出
78
+ data = self.storage._safe_json_loads(entry.content)
79
+ if data is None:
80
+ continue
81
+ profile = UserProfile.from_dict(data)
82
+ self.profiles[profile.user_id] = profile
83
+ except (json.JSONDecodeError, TypeError, ValueError):
84
+ pass
85
+ except sqlite3.OperationalError as _prof_err:
86
+ logger.warning("加载用户画像失败,画像检索可能降级: %s", _prof_err)
87
+
88
+ def get_profile(self, user_id: str = "default") -> UserProfile:
89
+ """获取用户画像"""
90
+ if user_id not in self.profiles:
91
+ self.profiles[user_id] = UserProfile(user_id=user_id)
92
+ return self.profiles[user_id]
93
+
94
+ def learn_from_interaction(self, user_id: str,
95
+ user_message: str,
96
+ agent_response: str,
97
+ metadata: Optional[Dict] = None):
98
+ """从交互中学习"""
99
+ profile = self.get_profile(user_id)
100
+ profile.total_interactions += 1
101
+ profile.last_updated = time.time()
102
+
103
+ self._analyze_language_style(profile, user_message)
104
+ self._analyze_topics(profile, user_message)
105
+ self._analyze_technical_level(profile, user_message)
106
+ self._analyze_communication_patterns(profile, user_message)
107
+
108
+ profile.interaction_history.append({
109
+ "timestamp": time.time(),
110
+ "message_length": len(user_message),
111
+ "response_length": len(agent_response),
112
+ "metadata": metadata or {},
113
+ })
114
+
115
+ if len(profile.interaction_history) > 1000:
116
+ profile.interaction_history = profile.interaction_history[-1000:]
117
+
118
+ self._save_profile(profile)
119
+
120
+ def _analyze_language_style(self, profile: UserProfile, text: str):
121
+ """分析语言风格"""
122
+ style = profile.language_style
123
+
124
+ style["avg_message_length"] = (
125
+ style.get("avg_message_length", 0) * 0.9 + len(text) * 0.1
126
+ )
127
+
128
+ formality_score = self._calculate_formality(text)
129
+ style["formality"] = (
130
+ style.get("formality", 0.5) * 0.95 + formality_score * 0.05
131
+ )
132
+
133
+ emoji_count = sum(1 for ch in text if ord(ch) > 0x1F000)
134
+ style["emoji_usage"] = (
135
+ style.get("emoji_usage", 0) * 0.9 + min(emoji_count / 5, 1.0) * 0.1
136
+ )
137
+
138
+ exclamation_count = text.count('!')
139
+ style["excitement"] = (
140
+ style.get("excitement", 0.5) * 0.95 + min(exclamation_count / 3, 1.0) * 0.05
141
+ )
142
+
143
+ question_count = text.count('?')
144
+ style["curiosity"] = (
145
+ style.get("curiosity", 0.5) * 0.95 + min(question_count / 2, 1.0) * 0.05
146
+ )
147
+
148
+ def _calculate_formality(self, text: str) -> float:
149
+ """计算正式程度"""
150
+ formal_words = ['请', '您', '谢谢', '感谢', '请问', '您好', '尊敬', '此致', '敬礼']
151
+ casual_words = ['哈哈', '哈哈哈哈', 'emmm', 'emm', '啊这', 'yyds', '绝了', '哇']
152
+
153
+ formal_score = sum(1 for w in formal_words if w in text)
154
+ casual_score = sum(1 for w in casual_words if w in text)
155
+
156
+ total = formal_score + casual_score + 1
157
+ return 0.3 + 0.4 * (formal_score / total)
158
+
159
+ def _analyze_topics(self, profile: UserProfile, text: str):
160
+ """分析兴趣主题"""
161
+ topics = profile.topics_of_interest
162
+ text_lower = text.lower()
163
+
164
+ topic_keywords = {
165
+ "编程开发": ['代码', '编程', '开发', '函数', '类', '接口', 'api', 'bug', 'debug'],
166
+ "人工智能": ['ai', '人工智能', '大模型', 'llm', 'gpt', 'agent', '机器学习', '深度学习'],
167
+ "数据库": ['数据库', 'sql', 'mysql', 'postgresql', 'redis', 'mongodb', '索引', '查询'],
168
+ "前端开发": ['前端', 'html', 'css', 'javascript', 'react', 'vue', '组件', 'ui'],
169
+ "生活日常": ['吃饭', '睡觉', '周末', '假期', '旅游', '电影', '音乐', '游戏'],
170
+ "学习成长": ['学习', '研究', '阅读', '书籍', '课程', '教程', '成长', '进步'],
171
+ }
172
+
173
+ for topic, keywords in topic_keywords.items():
174
+ count = sum(1 for kw in keywords if kw in text_lower)
175
+ if count > 0:
176
+ current = topics.get(topic, 0)
177
+ topics[topic] = current + count * 0.1
178
+ if topics[topic] > 1.0:
179
+ topics[topic] = 1.0
180
+
181
+ def _analyze_technical_level(self, profile: UserProfile, text: str):
182
+ """分析技术水平"""
183
+ tech_level = profile.technical_level
184
+ text_lower = text.lower()
185
+
186
+ beginner_words = ['怎么用', '如何', '入门', '新手', '基础', '教程', '简单']
187
+ advanced_words = ['优化', '架构', '性能', '并发', '分布式', '微服务', '底层', '原理']
188
+
189
+ beginner_count = sum(1 for w in beginner_words if w in text_lower)
190
+ advanced_count = sum(1 for w in advanced_words if w in text_lower)
191
+
192
+ current = tech_level.get("overall", 0.5)
193
+ if advanced_count > beginner_count:
194
+ new_level = min(1.0, current + 0.02)
195
+ elif beginner_count > advanced_count:
196
+ new_level = max(0.0, current - 0.01)
197
+ else:
198
+ new_level = current
199
+
200
+ tech_level["overall"] = new_level
201
+
202
+ def _analyze_communication_patterns(self, profile: UserProfile, text: str):
203
+ """分析沟通模式"""
204
+ patterns = profile.communication_patterns
205
+
206
+ sentence_count = len(re.split(r'[。!?.!?]', text))
207
+ patterns["avg_sentences_per_message"] = (
208
+ patterns.get("avg_sentences_per_message", 3) * 0.9 + sentence_count * 0.1
209
+ )
210
+
211
+ detail_length = len(text) / max(sentence_count, 1)
212
+ patterns["detail_oriented"] = (
213
+ patterns.get("detail_oriented", 0.5) * 0.95
214
+ + min(detail_length / 100, 1.0) * 0.05
215
+ )
216
+
217
+ def get_recommended_style(self, user_id: str = "default") -> Dict:
218
+ """获取推荐的交流风格"""
219
+ profile = self.get_profile(user_id)
220
+ style = profile.language_style
221
+
222
+ formality = style.get("formality", 0.5)
223
+ emoji_usage = style.get("emoji_usage", 0)
224
+ detail = style.get("detail_oriented", 0.5)
225
+
226
+ return {
227
+ "formality_level": "正式" if formality > 0.7 else "中性" if formality > 0.4 else "轻松",
228
+ "use_emoji": emoji_usage > 0.3,
229
+ "detail_level": "详细" if detail > 0.7 else "适中" if detail > 0.4 else "简洁",
230
+ "response_length": "详细" if detail > 0.6 else "标准",
231
+ "technical_depth": profile.technical_level.get("overall", 0.5),
232
+ }
233
+
234
+ def get_top_interests(self, user_id: str = "default", top_n: int = 5) -> List[Tuple[str, float]]:
235
+ """获取最感兴趣的主题"""
236
+ profile = self.get_profile(user_id)
237
+ interests = sorted(profile.topics_of_interest.items(), key=lambda x: x[1], reverse=True)
238
+ return interests[:top_n]
239
+
240
+ def _save_profile(self, profile: UserProfile):
241
+ """保存用户画像"""
242
+ try:
243
+ existing = self.storage.list_memories(
244
+ category="user_profile", limit=10
245
+ )
246
+
247
+ profile_entry = None
248
+ for entry in existing:
249
+ try:
250
+ # P3 #29 修复:使用安全 JSON 解析
251
+ data = self.storage._safe_json_loads(entry.content)
252
+ if data is None:
253
+ continue
254
+ if data.get("user_id") == profile.user_id:
255
+ profile_entry = entry
256
+ break
257
+ except (json.JSONDecodeError, TypeError, ValueError):
258
+ pass
259
+
260
+ content = json.dumps(profile.to_dict(), ensure_ascii=False)
261
+
262
+ if profile_entry:
263
+ self.storage.update_memory(
264
+ entry_id=profile_entry.id,
265
+ content=content,
266
+ )
267
+ else:
268
+ self.storage.add_memory(
269
+ content=content,
270
+ category="user_profile",
271
+ tags=["profile", profile.user_id],
272
+ source_agent="personality_engine",
273
+ )
274
+ except (sqlite3.OperationalError, ValueError) as _prof_err:
275
+ logger.warning("持久化用户画像失败: %s", _prof_err)
@@ -0,0 +1,258 @@
1
+ """
2
+ MindForge v6.0.0 功能 7:有原则的遗忘(可解释)
3
+
4
+ 不是删数据,而是像人一样:噪音自然淡出、重要模式留下来,
5
+ 并且能解释"为什么忘了这段"。
6
+
7
+ - explain_forgetting(): 对单条记忆给出可解释的遗忘评分分解
8
+ (时效衰减 / 访问频率 / 重要度保护 / 结果标记 / 噪音判定)
9
+ - run_principled_gc(): 执行有原则的遗忘 GC(复用 MemoryDecayEngine),
10
+ 输出每条被归档记忆的遗忘原因(explainable)
11
+ - forgetting_stats(): 遗忘引擎统计
12
+
13
+ 设计:
14
+ - 复用 mindforge.modules.memory_decay.DecayConfig / MemoryDecayEngine
15
+ - 每条遗忘决策附带 reason 列表(人类可读)
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import time
21
+ from typing import Any, Dict, List, Optional
22
+
23
+ _HALF_LIFE_DAYS = 30.0
24
+
25
+
26
+ def _decay_score(storage, entry, now: float) -> Dict[str, Any]:
27
+ """计算单条记忆的遗忘评分分解"""
28
+ try:
29
+ from mindforge.modules.memory_decay import MemoryDecayEngine
30
+ engine = MemoryDecayEngine(storage)
31
+ except Exception:
32
+ engine = None
33
+
34
+ reasons = []
35
+ factors = {}
36
+
37
+ # 1. 时效衰减(最后访问距今)
38
+ last_accessed = float(getattr(entry, "last_accessed_at", 0) or 0)
39
+ created = float(getattr(entry, "created_at", 0) or 0)
40
+ updated = float(getattr(entry, "updated_at", 0) or 0)
41
+ ref_ts = max(last_accessed, updated, created) or now
42
+ age_days = max(0.0, (now - ref_ts) / 86400.0)
43
+ recency = 1.0 / (1.0 + age_days / _HALF_LIFE_DAYS)
44
+ factors["recency"] = round(recency, 4)
45
+ if age_days > _HALF_LIFE_DAYS * 2:
46
+ reasons.append(f"超过 {int(age_days)} 天未被访问/更新,时效衰减")
47
+
48
+ # 2. 访问频率
49
+ access_count = int(getattr(entry, "access_count", 0) or 0)
50
+ frequency = min(1.0, access_count / 10.0)
51
+ factors["frequency"] = round(frequency, 4)
52
+ if access_count == 0 and age_days > 7:
53
+ reasons.append("从未被检索/访问,缺乏使用证据")
54
+
55
+ # 3. 重要度保护
56
+ importance_val = getattr(entry, "importance", "") or "MEDIUM"
57
+ importance = getattr(importance_val, "value", importance_val)
58
+ importance = str(importance or "MEDIUM").upper()
59
+ importance_protect = {"HIGH": 0.9, "MEDIUM": 0.5, "LOW": 0.2}.get(importance, 0.5)
60
+ factors["importance_protection"] = importance_protect
61
+ if importance == "HIGH":
62
+ reasons.append("重要度 HIGH:受保护,不易遗忘")
63
+ elif importance == "LOW":
64
+ reasons.append("重要度 LOW:弱保护,更容易淡出")
65
+
66
+ # 4. 结果标记(功能 5 联动):success 记忆保留、failure 记忆允许淡出
67
+ outcome = ""
68
+ try:
69
+ conn = storage._get_conn()
70
+ row = conn.execute(
71
+ "SELECT outcome FROM memories WHERE id = ?",
72
+ (getattr(entry, "id", ""),)).fetchone()
73
+ outcome = row[0] if row else ""
74
+ except Exception:
75
+ pass
76
+ outcome_boost = {"success": 0.3, "partial": 0.1, "failure": -0.2}.get(outcome, 0.0)
77
+ factors["outcome_boost"] = outcome_boost
78
+ if outcome == "success":
79
+ reasons.append("结果标记 success:优先保留")
80
+ elif outcome == "failure":
81
+ reasons.append("结果标记 failure:允许降权淡出")
82
+
83
+ # 5. 噪声判定(短且无实义)
84
+ content = (getattr(entry, "content", "") or "").strip()
85
+ # v6.1.2:原元组里 "住在" 写了两次(笔误,去重后判定结果不变)
86
+ is_noisy = len(content) < 12 and not any(
87
+ k in content for k in ("工作", "喜欢", "住在", "是", "目标"))
88
+ factors["noise"] = 1.0 if is_noisy else 0.0
89
+ if is_noisy:
90
+ reasons.append("内容过短且无明确信息量(噪声型记忆)")
91
+
92
+ # 综合遗忘倾向(0~1,越高越该遗忘)
93
+ forget_tendency = round(
94
+ (1.0 - recency) * 0.45
95
+ + (1.0 - frequency) * 0.25
96
+ + (1.0 - importance_protect) * 0.2
97
+ + (1.0 - (0.5 + outcome_boost)) * 0.1, 4)
98
+ factors["forget_tendency"] = forget_tendency
99
+ factors["age_days"] = round(age_days, 2)
100
+ factors["access_count"] = access_count
101
+
102
+ if engine is not None:
103
+ try:
104
+ strength = engine.compute_strength(entry)
105
+ factors["engine_strength"] = round(float(strength or 0.0), 4)
106
+ except Exception:
107
+ pass
108
+ return {"factors": factors, "reasons": reasons,
109
+ "forget_tendency": forget_tendency}
110
+
111
+
112
+ def _coerce_memory_id(memory_id: Any) -> tuple:
113
+ """接受 str 或 add() 返回的 MemoryEntry,返回 (id, error)。
114
+
115
+ v6.0.0 直接 len(memory_id),传对象会抛 TypeError 而非友好报错(v6.1.1 修)。
116
+ """
117
+ if memory_id is None:
118
+ return None, "记忆 ID 无效"
119
+ if not isinstance(memory_id, str):
120
+ memory_id = getattr(memory_id, "id", None)
121
+ if not isinstance(memory_id, str):
122
+ return None, "记忆 ID 必须是字符串或 add() 返回的 MemoryEntry 对象"
123
+ if not memory_id or len(memory_id) > 128:
124
+ return None, "记忆 ID 无效"
125
+ return memory_id, None
126
+
127
+
128
+ def explain_forgetting(storage, memory_id: str,
129
+ actor: str = "", session_id: str = "") -> Dict[str, Any]:
130
+ """解释一条记忆的遗忘决策"""
131
+ memory_id, id_error = _coerce_memory_id(memory_id)
132
+ if id_error:
133
+ return {"success": False, "error": id_error}
134
+ entry = storage.get_memory(memory_id, actor, session_id)
135
+ if entry is None:
136
+ return {"success": False, "error": "记忆不存在"}
137
+ now = time.time()
138
+ d = _decay_score(storage, entry, now)
139
+ return {
140
+ "success": True,
141
+ "memory_id": memory_id,
142
+ "content_preview": (getattr(entry, "content", "") or "")[:120],
143
+ "explanation": d["reasons"],
144
+ "forget_tendency": d["forget_tendency"],
145
+ "factors": d["factors"],
146
+ "privacy": "aggregate-only",
147
+ }
148
+
149
+
150
+ def run_principled_gc(mf, dry_run: bool = False, days: int = 30) -> Dict[str, Any]:
151
+ """执行有原则的遗忘 GC:噪音淡出、重要模式保留,输出可解释决策。
152
+
153
+ - 遍历未归档记忆,计算 forget_tendency
154
+ - 阈值以上且非保护记忆 → 归档(archive),并记录原因
155
+
156
+ v6.1.2 修复:`days` 参数自 v6.0.0 起就在签名与门面里一路传进来,函数体却
157
+ 从未引用它(阈值 0.72 硬编码,年龄窗口全靠 forget_tendency 隐式体现),
158
+ 调用者以为在调「GC 年龄窗口」,实际改了个寂寞。现在 `days` 是真正的**冷却
159
+ 窗口**:创建不足 `days` 天的记忆不参与归档,并单独计入 `skipped_too_fresh`;
160
+ `days <= 0` 表示关闭冷却(用于清理刚写入的成批噪音)。
161
+ """
162
+ try:
163
+ from mindforge.modules.memory_decay import MemoryDecayEngine
164
+ engine = MemoryDecayEngine(mf._storage)
165
+ except Exception:
166
+ engine = None
167
+
168
+ try:
169
+ min_age_days = int(days)
170
+ except (TypeError, ValueError):
171
+ min_age_days = 30
172
+ min_age_seconds = max(0, min_age_days) * 86400.0
173
+
174
+ now = time.time()
175
+ entries = mf._storage.list_memories(limit=10000) or []
176
+ threshold = 0.72
177
+ archived = []
178
+ protected = 0
179
+ scanned = 0
180
+ too_fresh = 0
181
+
182
+ for entry in entries:
183
+ imp_val = getattr(entry, "importance", "") or "MEDIUM"
184
+ importance = str(getattr(imp_val, "value", imp_val) or "MEDIUM").upper()
185
+ if importance == "HIGH":
186
+ protected += 1
187
+ continue
188
+ if min_age_seconds > 0:
189
+ created_at = getattr(entry, "created_at", 0) or 0
190
+ if now - float(created_at) < min_age_seconds:
191
+ too_fresh += 1
192
+ continue
193
+ scanned += 1
194
+ d = _decay_score(mf._storage, entry, now)
195
+ if d["forget_tendency"] < threshold:
196
+ continue
197
+ reasons = d["reasons"] or ["长期未使用,噪音淡出"]
198
+ mem_id = getattr(entry, "id", "")
199
+ if not dry_run:
200
+ ok = False
201
+ try:
202
+ if engine is not None:
203
+ ok = bool(engine.storage.archive_memories_by_ids(
204
+ [mem_id], reason="principled_forgetting", actor="gc") or 0)
205
+ if not ok:
206
+ ok = bool(mf._storage.archive_memories_by_ids(
207
+ [mem_id], reason="principled_forgetting", actor="gc") or 0)
208
+ except Exception:
209
+ try:
210
+ ok = bool(mf._storage.archive_memories_by_ids(
211
+ [mem_id], reason="principled_forgetting", actor="gc") or 0)
212
+ except Exception:
213
+ ok = False
214
+ else:
215
+ ok = True
216
+ archived.append({
217
+ "memory_id": mem_id,
218
+ "content_preview": (getattr(entry, "content", "") or "")[:80],
219
+ "tendency": d["forget_tendency"],
220
+ "reasons": reasons,
221
+ "archived": ok,
222
+ })
223
+ if len(archived) >= 200:
224
+ break
225
+
226
+ return {
227
+ "success": True,
228
+ "dry_run": bool(dry_run),
229
+ "days": min_age_days,
230
+ "scanned": scanned,
231
+ "skipped_too_fresh": too_fresh,
232
+ "protected_high_importance": protected,
233
+ "candidates": len(archived),
234
+ "archived": sum(1 for a in archived if a["archived"]),
235
+ "decisions": archived[:50],
236
+ "threshold": threshold,
237
+ }
238
+
239
+
240
+ def forgetting_stats(mf) -> Dict[str, Any]:
241
+ """遗忘引擎统计"""
242
+ stats = {"total_memories": 0, "archived": 0, "high_importance": 0,
243
+ "outcome_marked": 0, "explainable": True}
244
+ try:
245
+ conn = mf._storage._get_conn()
246
+ row = conn.execute("SELECT COUNT(*) FROM memories").fetchone()
247
+ stats["total_memories"] = row[0] if row else 0
248
+ row = conn.execute(
249
+ "SELECT COUNT(*) FROM memories WHERE importance = 'HIGH'").fetchone()
250
+ stats["high_importance"] = row[0] if row else 0
251
+ row = conn.execute(
252
+ "SELECT COUNT(*) FROM memories WHERE outcome != ''").fetchone()
253
+ stats["outcome_marked"] = row[0] if row else 0
254
+ row = conn.execute("SELECT COUNT(*) FROM archived_memories").fetchone()
255
+ stats["archived"] = row[0] if row else 0
256
+ except Exception:
257
+ pass
258
+ return stats