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.
- MindForge.py +20 -0
- mindforge/__init__.py +111 -0
- mindforge/adapters/__init__.py +18 -0
- mindforge/adapters/claude_adapter.py +84 -0
- mindforge/adapters/crewai_memory.py +76 -0
- mindforge/adapters/generic_api.py +183 -0
- mindforge/adapters/langgraph_store.py +463 -0
- mindforge/adapters/openclaw_adapter.py +91 -0
- mindforge/api/__init__.py +1 -0
- mindforge/api/server.py +1567 -0
- mindforge/cli/__init__.py +1 -0
- mindforge/cli/main.py +13607 -0
- mindforge/core/__init__.py +37 -0
- mindforge/core/embedding.py +847 -0
- mindforge/core/encryption.py +597 -0
- mindforge/core/indexer.py +363 -0
- mindforge/core/mindforge.py +6298 -0
- mindforge/core/paths.py +43 -0
- mindforge/core/query.py +501 -0
- mindforge/core/storage.py +18656 -0
- mindforge/core/types.py +314 -0
- mindforge/core/version.py +56 -0
- mindforge/daemon/__init__.py +0 -0
- mindforge/daemon/server.py +277 -0
- mindforge/mcp/__init__.py +4 -0
- mindforge/mcp/server.py +2559 -0
- mindforge/modules/__init__.py +138 -0
- mindforge/modules/categorizer.py +151 -0
- mindforge/modules/conflict_detector.py +356 -0
- mindforge/modules/connectors.py +331 -0
- mindforge/modules/context_window.py +337 -0
- mindforge/modules/event_bus.py +572 -0
- mindforge/modules/evolution.py +210 -0
- mindforge/modules/experience.py +402 -0
- mindforge/modules/federated.py +722 -0
- mindforge/modules/federated_acl.py +393 -0
- mindforge/modules/follow_up.py +315 -0
- mindforge/modules/hybrid_search.py +565 -0
- mindforge/modules/importer.py +374 -0
- mindforge/modules/integrator.py +196 -0
- mindforge/modules/intent_router.py +359 -0
- mindforge/modules/knowledge_graph.py +395 -0
- mindforge/modules/memory_compressor.py +592 -0
- mindforge/modules/memory_decay.py +474 -0
- mindforge/modules/memory_passport.py +250 -0
- mindforge/modules/multi_agent.py +534 -0
- mindforge/modules/multimodal.py +304 -0
- mindforge/modules/outcome_learning.py +186 -0
- mindforge/modules/personality.py +275 -0
- mindforge/modules/principled_forgetting.py +258 -0
- mindforge/modules/privacy.py +736 -0
- mindforge/modules/recall.py +263 -0
- mindforge/modules/session_focus.py +305 -0
- mindforge/modules/share_conflict.py +521 -0
- mindforge/modules/skill_extractor.py +340 -0
- mindforge/modules/skill_pack.py +205 -0
- mindforge/modules/timeline_viz.py +206 -0
- mindforge/modules/v6.py +931 -0
- mindforge_memory-6.4.1.dist-info/METADATA +281 -0
- mindforge_memory-6.4.1.dist-info/RECORD +64 -0
- mindforge_memory-6.4.1.dist-info/WHEEL +5 -0
- mindforge_memory-6.4.1.dist-info/entry_points.txt +3 -0
- mindforge_memory-6.4.1.dist-info/licenses/LICENSE +21 -0
- 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
|