doudou-memory 1.0.0__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.
- doudou_memory/__init__.py +64 -0
- doudou_memory/core/forgetting_mechanism.py +312 -0
- doudou_memory/core/intelligent_manager.py +339 -0
- doudou_memory/core/intelligent_memory_manager.py +411 -0
- doudou_memory/core/memory_system.py +165 -0
- doudou_memory/core/review_scheduler.py +130 -0
- doudou_memory/core/working_memory.py +218 -0
- doudou_memory-1.0.0.dist-info/METADATA +292 -0
- doudou_memory-1.0.0.dist-info/RECORD +12 -0
- doudou_memory-1.0.0.dist-info/WHEEL +5 -0
- doudou_memory-1.0.0.dist-info/licenses/LICENSE +21 -0
- doudou_memory-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
豆豆记忆系统
|
|
3
|
+
Doudou Memory System
|
|
4
|
+
|
|
5
|
+
为 AI Agent 打造的科学记忆系统
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "1.0.0"
|
|
9
|
+
__author__ = "Doudou Software Studio"
|
|
10
|
+
|
|
11
|
+
from .core.memory_system import (
|
|
12
|
+
MemorySystem,
|
|
13
|
+
memorize,
|
|
14
|
+
recall,
|
|
15
|
+
get_memory_system
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from .core.working_memory import (
|
|
19
|
+
WorkingMemory,
|
|
20
|
+
get_working_memory
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from .core.forgetting_mechanism import (
|
|
24
|
+
DecayManager,
|
|
25
|
+
get_decay_manager,
|
|
26
|
+
EbbinghausForgetting
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
from .core.review_scheduler import (
|
|
30
|
+
ReviewScheduler,
|
|
31
|
+
get_review_scheduler
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
from .core.intelligent_manager import (
|
|
35
|
+
IntelligentMemoryManager,
|
|
36
|
+
smart_memorize
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
# 核心系统
|
|
41
|
+
"MemorySystem",
|
|
42
|
+
"get_memory_system",
|
|
43
|
+
|
|
44
|
+
# 工作记忆
|
|
45
|
+
"WorkingMemory",
|
|
46
|
+
"get_working_memory",
|
|
47
|
+
|
|
48
|
+
# 遗忘机制
|
|
49
|
+
"DecayManager",
|
|
50
|
+
"get_decay_manager",
|
|
51
|
+
"EbbinghausForgetting",
|
|
52
|
+
|
|
53
|
+
# 复习调度
|
|
54
|
+
"ReviewScheduler",
|
|
55
|
+
"get_review_scheduler",
|
|
56
|
+
|
|
57
|
+
# 智能管理
|
|
58
|
+
"IntelligentMemoryManager",
|
|
59
|
+
"smart_memorize",
|
|
60
|
+
|
|
61
|
+
# 便捷函数
|
|
62
|
+
"memorize",
|
|
63
|
+
"recall",
|
|
64
|
+
]
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""
|
|
2
|
+
L3/L4 遗忘机制模块
|
|
3
|
+
Forgetting Mechanism Module
|
|
4
|
+
|
|
5
|
+
基于 Ebbinghaus 遗忘曲线
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import math
|
|
10
|
+
import logging
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
from typing import Dict, List, Optional
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# 默认存储路径
|
|
18
|
+
DEFAULT_STORAGE_PATH = "D:/openclaw-agents/shared/experience/long_term_memory"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EbbinghausForgetting:
|
|
22
|
+
"""
|
|
23
|
+
Ebbinghaus 遗忘曲线计算器
|
|
24
|
+
|
|
25
|
+
公式: R = e^(-t/S)
|
|
26
|
+
- R: 保留率
|
|
27
|
+
- t: 时间(小时)
|
|
28
|
+
- S: 记忆强度(半衰期)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
# 标准遗忘曲线数据点
|
|
32
|
+
FORGETTING_CURVE = {
|
|
33
|
+
1/3: 58.2, # 20分钟
|
|
34
|
+
1: 44.2, # 1小时
|
|
35
|
+
24: 33.7, # 1天
|
|
36
|
+
24 * 7: 25.4, # 1周
|
|
37
|
+
24 * 31: 21.1 # 31天
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
def __init__(self, min_retention: float = 0.3):
|
|
41
|
+
self.min_retention = min_retention
|
|
42
|
+
|
|
43
|
+
def calculate_retention(self, age_hours: float,
|
|
44
|
+
strength: float = 24) -> float:
|
|
45
|
+
"""
|
|
46
|
+
计算保留率
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
age_hours: 记忆年龄(小时)
|
|
50
|
+
strength: 记忆强度(小时)
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
保留率 (0-1)
|
|
54
|
+
"""
|
|
55
|
+
if age_hours <= 0:
|
|
56
|
+
return 1.0
|
|
57
|
+
|
|
58
|
+
retention = math.exp(-age_hours / strength)
|
|
59
|
+
|
|
60
|
+
# 确保不低于最小保留率
|
|
61
|
+
return max(self.min_retention, min(1.0, retention))
|
|
62
|
+
|
|
63
|
+
def calculate_strength(self, retention: float, age_hours: float) -> float:
|
|
64
|
+
"""根据保留率计算记忆强度"""
|
|
65
|
+
if retention <= 0 or retention >= 1:
|
|
66
|
+
return float('inf')
|
|
67
|
+
|
|
68
|
+
return -age_hours / math.log(retention)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class MemoryRecord:
|
|
72
|
+
"""记忆记录"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, memory_id: str, content: str,
|
|
75
|
+
importance: float = 0.5, memory_type: str = "episodic"):
|
|
76
|
+
self.id = memory_id
|
|
77
|
+
self.content = content
|
|
78
|
+
self.importance = importance
|
|
79
|
+
self.memory_type = memory_type
|
|
80
|
+
self.created_at = datetime.now()
|
|
81
|
+
self.last_accessed = datetime.now()
|
|
82
|
+
self.access_count = 0
|
|
83
|
+
self.retention_score = 1.0
|
|
84
|
+
self.review_count = 0
|
|
85
|
+
self.next_review: Optional[datetime] = None
|
|
86
|
+
self.archived = False
|
|
87
|
+
|
|
88
|
+
# SM-2 参数
|
|
89
|
+
self.easy_factor = 2.5
|
|
90
|
+
self.interval = 1 # 天数
|
|
91
|
+
|
|
92
|
+
def to_dict(self) -> Dict:
|
|
93
|
+
return {
|
|
94
|
+
"id": self.id,
|
|
95
|
+
"content": self.content,
|
|
96
|
+
"importance": self.importance,
|
|
97
|
+
"memory_type": self.memory_type,
|
|
98
|
+
"created_at": self.created_at.isoformat(),
|
|
99
|
+
"last_accessed": self.last_accessed.isoformat(),
|
|
100
|
+
"access_count": self.access_count,
|
|
101
|
+
"retention_score": self.retention_score,
|
|
102
|
+
"review_count": self.review_count,
|
|
103
|
+
"next_review": self.next_review.isoformat() if self.next_review else None,
|
|
104
|
+
"archived": self.archived,
|
|
105
|
+
"easy_factor": self.easy_factor,
|
|
106
|
+
"interval": self.interval
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def from_dict(cls, data: Dict) -> 'MemoryRecord':
|
|
111
|
+
record = cls(
|
|
112
|
+
memory_id=data["id"],
|
|
113
|
+
content=data["content"],
|
|
114
|
+
importance=data.get("importance", 0.5),
|
|
115
|
+
memory_type=data.get("memory_type", "episodic")
|
|
116
|
+
)
|
|
117
|
+
record.created_at = datetime.fromisoformat(data["created_at"])
|
|
118
|
+
record.last_accessed = datetime.fromisoformat(data["last_accessed"])
|
|
119
|
+
record.access_count = data.get("access_count", 0)
|
|
120
|
+
record.retention_score = data.get("retention_score", 1.0)
|
|
121
|
+
record.review_count = data.get("review_count", 0)
|
|
122
|
+
if data.get("next_review"):
|
|
123
|
+
record.next_review = datetime.fromisoformat(data["next_review"])
|
|
124
|
+
record.archived = data.get("archived", False)
|
|
125
|
+
record.easy_factor = data.get("easy_factor", 2.5)
|
|
126
|
+
record.interval = data.get("interval", 1)
|
|
127
|
+
return record
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class DecayManager:
|
|
131
|
+
"""
|
|
132
|
+
衰减管理器
|
|
133
|
+
|
|
134
|
+
管理长期记忆的遗忘衰减和间隔重复
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def __init__(self, storage_path: str = DEFAULT_STORAGE_PATH):
|
|
138
|
+
self.storage_path = Path(storage_path)
|
|
139
|
+
self.storage_path.mkdir(parents=True, exist_ok=True)
|
|
140
|
+
|
|
141
|
+
self.memories: Dict[str, MemoryRecord] = {}
|
|
142
|
+
self.ebbinghaus = EbbinghausForgetting()
|
|
143
|
+
|
|
144
|
+
self._load_memories()
|
|
145
|
+
|
|
146
|
+
def register_memory(self, memory_id: str, content: str,
|
|
147
|
+
importance: float = 0.5,
|
|
148
|
+
memory_type: str = "episodic") -> str:
|
|
149
|
+
"""注册新记忆"""
|
|
150
|
+
record = MemoryRecord(
|
|
151
|
+
memory_id=memory_id,
|
|
152
|
+
content=content,
|
|
153
|
+
importance=importance,
|
|
154
|
+
memory_type=memory_type
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# 设置首次复习时间
|
|
158
|
+
record.next_review = datetime.now() + timedelta(days=1)
|
|
159
|
+
|
|
160
|
+
self.memories[memory_id] = record
|
|
161
|
+
self._save_memories()
|
|
162
|
+
|
|
163
|
+
logger.info(f"[衰减管理] 注册记忆: {memory_id}")
|
|
164
|
+
|
|
165
|
+
return memory_id
|
|
166
|
+
|
|
167
|
+
def get_memory(self, memory_id: str) -> Optional[Dict]:
|
|
168
|
+
"""获取记忆"""
|
|
169
|
+
record = self.memories.get(memory_id)
|
|
170
|
+
if record and not record.archived:
|
|
171
|
+
record.last_accessed = datetime.now()
|
|
172
|
+
record.access_count += 1
|
|
173
|
+
return record.to_dict()
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
def reinforce_memory(self, memory_id: str, quality: int = 3):
|
|
177
|
+
"""
|
|
178
|
+
强化记忆(SM-2 算法)
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
memory_id: 记忆ID
|
|
182
|
+
quality: 回忆质量 (0-5)
|
|
183
|
+
"""
|
|
184
|
+
record = self.memories.get(memory_id)
|
|
185
|
+
if not record:
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
# SM-2 算法
|
|
189
|
+
if quality >= 3:
|
|
190
|
+
# 成功回忆
|
|
191
|
+
if record.review_count == 0:
|
|
192
|
+
record.interval = 1
|
|
193
|
+
elif record.review_count == 1:
|
|
194
|
+
record.interval = 3
|
|
195
|
+
else:
|
|
196
|
+
record.interval = int(record.interval * record.easy_factor)
|
|
197
|
+
|
|
198
|
+
# 更新 Easy Factor
|
|
199
|
+
record.easy_factor = max(1.3, record.easy_factor + (0.1 - (5 - quality) * 0.08))
|
|
200
|
+
else:
|
|
201
|
+
# 失败回忆
|
|
202
|
+
record.interval = 1
|
|
203
|
+
record.easy_factor = max(1.3, record.easy_factor - 0.2)
|
|
204
|
+
|
|
205
|
+
record.review_count += 1
|
|
206
|
+
record.next_review = datetime.now() + timedelta(days=record.interval)
|
|
207
|
+
record.retention_score = 1.0
|
|
208
|
+
record.last_accessed = datetime.now()
|
|
209
|
+
|
|
210
|
+
self._save_memories()
|
|
211
|
+
|
|
212
|
+
logger.info(f"[衰减管理] 强化记忆: {memory_id}, 下次复习: {record.next_review.strftime('%Y-%m-%d')}")
|
|
213
|
+
|
|
214
|
+
def apply_decay(self) -> List[str]:
|
|
215
|
+
"""
|
|
216
|
+
应用遗忘衰减
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
归档的记忆ID列表
|
|
220
|
+
"""
|
|
221
|
+
archived_ids = []
|
|
222
|
+
now = datetime.now()
|
|
223
|
+
|
|
224
|
+
for memory_id, record in self.memories.items():
|
|
225
|
+
if record.archived:
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
# 计算年龄
|
|
229
|
+
age_hours = (now - record.created_at).total_seconds() / 3600
|
|
230
|
+
|
|
231
|
+
# 计算保留率
|
|
232
|
+
record.retention_score = self.ebbinghaus.calculate_retention(age_hours)
|
|
233
|
+
|
|
234
|
+
# 检查是否归档
|
|
235
|
+
if record.retention_score < 0.3 and record.importance < 0.6:
|
|
236
|
+
record.archived = True
|
|
237
|
+
archived_ids.append(memory_id)
|
|
238
|
+
logger.info(f"[衰减管理] 归档: {memory_id}")
|
|
239
|
+
|
|
240
|
+
self._save_memories()
|
|
241
|
+
|
|
242
|
+
return archived_ids
|
|
243
|
+
|
|
244
|
+
def get_memories_for_review(self) -> List[Dict]:
|
|
245
|
+
"""获取需要复习的记忆"""
|
|
246
|
+
now = datetime.now()
|
|
247
|
+
reviews = []
|
|
248
|
+
|
|
249
|
+
for memory_id, record in self.memories.items():
|
|
250
|
+
if record.archived:
|
|
251
|
+
continue
|
|
252
|
+
|
|
253
|
+
if record.next_review and record.next_review <= now:
|
|
254
|
+
reviews.append(record.to_dict())
|
|
255
|
+
|
|
256
|
+
return reviews
|
|
257
|
+
|
|
258
|
+
def get_stats(self) -> Dict:
|
|
259
|
+
"""获取统计"""
|
|
260
|
+
total = len(self.memories)
|
|
261
|
+
archived = sum(1 for m in self.memories.values() if m.archived)
|
|
262
|
+
|
|
263
|
+
retention_scores = [m.retention_score for m in self.memories.values() if not m.archived]
|
|
264
|
+
avg_retention = sum(retention_scores) / len(retention_scores) if retention_scores else 1.0
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
"total_memories": total,
|
|
268
|
+
"active_memories": total - archived,
|
|
269
|
+
"archived_count": archived,
|
|
270
|
+
"average_retention": round(avg_retention, 2)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
def _load_memories(self):
|
|
274
|
+
"""加载记忆"""
|
|
275
|
+
memory_file = self.storage_path / "memories.json"
|
|
276
|
+
|
|
277
|
+
if memory_file.exists():
|
|
278
|
+
try:
|
|
279
|
+
with open(memory_file, "r", encoding="utf-8") as f:
|
|
280
|
+
data = json.load(f)
|
|
281
|
+
|
|
282
|
+
for memory_id, record_data in data.items():
|
|
283
|
+
self.memories[memory_id] = MemoryRecord.from_dict(record_data)
|
|
284
|
+
|
|
285
|
+
logger.info(f"[衰减管理] 加载 {len(self.memories)} 条记忆")
|
|
286
|
+
except Exception as e:
|
|
287
|
+
logger.error(f"[衰减管理] 加载失败: {e}")
|
|
288
|
+
|
|
289
|
+
def _save_memories(self):
|
|
290
|
+
"""保存记忆"""
|
|
291
|
+
memory_file = self.storage_path / "memories.json"
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
data = {mid: record.to_dict() for mid, record in self.memories.items()}
|
|
295
|
+
|
|
296
|
+
with open(memory_file, "w", encoding="utf-8") as f:
|
|
297
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
298
|
+
except Exception as e:
|
|
299
|
+
logger.error(f"[衰减管理] 保存失败: {e}")
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# ==================== 全局实例 ====================
|
|
303
|
+
|
|
304
|
+
_decay_manager_instance = None
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def get_decay_manager() -> DecayManager:
|
|
308
|
+
"""获取衰减管理器实例"""
|
|
309
|
+
global _decay_manager_instance
|
|
310
|
+
if _decay_manager_instance is None:
|
|
311
|
+
_decay_manager_instance = DecayManager()
|
|
312
|
+
return _decay_manager_instance
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""
|
|
2
|
+
智能记忆管理器
|
|
3
|
+
Intelligent Memory Manager
|
|
4
|
+
|
|
5
|
+
功能:
|
|
6
|
+
1. 自动提取:从对话中自动提取关键记忆
|
|
7
|
+
2. 操作决策:决定 ADD/UPDATE/DELETE/NOOP
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
import logging
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# 默认 API 配置
|
|
20
|
+
DEFAULT_API_KEY = ""
|
|
21
|
+
DEFAULT_MODEL = "glm-5"
|
|
22
|
+
DEFAULT_API_URL = "https://coding.dashscope.aliyuncs.com/v1/chat/completions"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class IntelligentMemoryManager:
|
|
26
|
+
"""
|
|
27
|
+
智能记忆管理器
|
|
28
|
+
|
|
29
|
+
实现自动提取和操作决策
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, api_key: str = None, model: str = DEFAULT_MODEL):
|
|
33
|
+
self.api_key = api_key or DEFAULT_API_KEY
|
|
34
|
+
self.model = model
|
|
35
|
+
self.api_url = DEFAULT_API_URL
|
|
36
|
+
|
|
37
|
+
# 延迟加载
|
|
38
|
+
self._decay_manager = None
|
|
39
|
+
self._working_memory = None
|
|
40
|
+
|
|
41
|
+
def _get_decay_manager(self):
|
|
42
|
+
"""延迟加载衰减管理器"""
|
|
43
|
+
if self._decay_manager is None:
|
|
44
|
+
from .forgetting_mechanism import get_decay_manager
|
|
45
|
+
self._decay_manager = get_decay_manager()
|
|
46
|
+
return self._decay_manager
|
|
47
|
+
|
|
48
|
+
def _get_working_memory(self):
|
|
49
|
+
"""延迟加载工作记忆"""
|
|
50
|
+
if self._working_memory is None:
|
|
51
|
+
from .working_memory import get_working_memory
|
|
52
|
+
self._working_memory = get_working_memory()
|
|
53
|
+
return self._working_memory
|
|
54
|
+
|
|
55
|
+
def _call_llm(self, prompt: str, max_tokens: int = 500) -> str:
|
|
56
|
+
"""调用 LLM"""
|
|
57
|
+
if not self.api_key:
|
|
58
|
+
logger.warning("[智能记忆] API Key 未配置")
|
|
59
|
+
return ""
|
|
60
|
+
|
|
61
|
+
headers = {
|
|
62
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
63
|
+
"Content-Type": "application/json"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
payload = {
|
|
67
|
+
"model": self.model,
|
|
68
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
69
|
+
"max_tokens": max_tokens
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
resp = requests.post(self.api_url, headers=headers, json=payload, timeout=60)
|
|
74
|
+
|
|
75
|
+
if resp.status_code == 200:
|
|
76
|
+
result = resp.json()
|
|
77
|
+
return result.get("choices", [{}])[0].get("message", {}).get("content", "")
|
|
78
|
+
else:
|
|
79
|
+
logger.error(f"[智能记忆] API 错误: {resp.status_code}")
|
|
80
|
+
return ""
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.error(f"[智能记忆] 调用失败: {e}")
|
|
83
|
+
return ""
|
|
84
|
+
|
|
85
|
+
def _extract_json(self, text: str) -> Optional[Dict]:
|
|
86
|
+
"""从文本中提取 JSON"""
|
|
87
|
+
try:
|
|
88
|
+
return json.loads(text)
|
|
89
|
+
except:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
# 查找 JSON 块
|
|
93
|
+
json_match = re.search(r'```json\s*([\s\S]*?)\s*```', text)
|
|
94
|
+
if json_match:
|
|
95
|
+
try:
|
|
96
|
+
return json.loads(json_match.group(1))
|
|
97
|
+
except:
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
# 查找裸 JSON
|
|
101
|
+
json_match = re.search(r'\{[\s\S]*\}', text)
|
|
102
|
+
if json_match:
|
|
103
|
+
try:
|
|
104
|
+
return json.loads(json_match.group())
|
|
105
|
+
except:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
def extract_memories(self, conversation: str,
|
|
111
|
+
context: str = "") -> List[Dict]:
|
|
112
|
+
"""
|
|
113
|
+
从对话中自动提取记忆
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
conversation: 对话内容
|
|
117
|
+
context: 额外上下文
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
提取的记忆列表
|
|
121
|
+
"""
|
|
122
|
+
prompt = f"""从以下对话中提取关键记忆信息,返回 JSON 格式:
|
|
123
|
+
|
|
124
|
+
对话:
|
|
125
|
+
{conversation}
|
|
126
|
+
|
|
127
|
+
上下文:{context}
|
|
128
|
+
|
|
129
|
+
要求:提取重要的事实、偏好、决策,格式如下:
|
|
130
|
+
{{"memories": [{{"content": "...", "type": "fact", "importance": 0.8}}]}}
|
|
131
|
+
|
|
132
|
+
只返回 JSON,不要其他内容。"""
|
|
133
|
+
|
|
134
|
+
response = self._call_llm(prompt, max_tokens=1000)
|
|
135
|
+
|
|
136
|
+
if not response:
|
|
137
|
+
return []
|
|
138
|
+
|
|
139
|
+
result = self._extract_json(response)
|
|
140
|
+
|
|
141
|
+
if result and "memories" in result:
|
|
142
|
+
memories = result["memories"]
|
|
143
|
+
logger.info(f"[智能记忆] 提取 {len(memories)} 条记忆")
|
|
144
|
+
return memories
|
|
145
|
+
|
|
146
|
+
return []
|
|
147
|
+
|
|
148
|
+
def decide_operations(self, new_memories: List[Dict],
|
|
149
|
+
existing_memories: List[Dict] = None) -> List[Dict]:
|
|
150
|
+
"""
|
|
151
|
+
决定记忆操作
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
new_memories: 新提取的记忆
|
|
155
|
+
existing_memories: 现有相关记忆
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
操作列表
|
|
159
|
+
"""
|
|
160
|
+
if not new_memories:
|
|
161
|
+
return []
|
|
162
|
+
|
|
163
|
+
# 如果没有现有记忆,全部 ADD
|
|
164
|
+
if not existing_memories:
|
|
165
|
+
return [{"action": "ADD", "memory": mem} for mem in new_memories]
|
|
166
|
+
|
|
167
|
+
# 构建描述
|
|
168
|
+
existing_desc = "\n".join([
|
|
169
|
+
f"{i+1}. \"{mem.get('content', '')}\" (id: {mem.get('id', 'unknown')})"
|
|
170
|
+
for i, mem in enumerate(existing_memories[:10])
|
|
171
|
+
])
|
|
172
|
+
|
|
173
|
+
new_desc = "\n".join([f"- {mem.get('content', '')}" for mem in new_memories])
|
|
174
|
+
|
|
175
|
+
prompt = f"""根据新信息和现有记忆,决定要执行的操作:
|
|
176
|
+
|
|
177
|
+
现有记忆:
|
|
178
|
+
{existing_desc}
|
|
179
|
+
|
|
180
|
+
新信息:
|
|
181
|
+
{new_desc}
|
|
182
|
+
|
|
183
|
+
请决定操作(返回 JSON):
|
|
184
|
+
{{"operations": [
|
|
185
|
+
{{"action": "ADD", "memory": {{"content": "...", "type": "fact", "importance": 0.8}}}},
|
|
186
|
+
{{"action": "UPDATE", "memory_id": "...", "new_content": "..."}},
|
|
187
|
+
{{"action": "DELETE", "memory_id": "..."}},
|
|
188
|
+
{{"action": "NOOP"}}
|
|
189
|
+
]}}
|
|
190
|
+
|
|
191
|
+
只返回 JSON。"""
|
|
192
|
+
|
|
193
|
+
response = self._call_llm(prompt, max_tokens=1000)
|
|
194
|
+
|
|
195
|
+
if not response:
|
|
196
|
+
# 降级:全部 ADD
|
|
197
|
+
return [{"action": "ADD", "memory": mem} for mem in new_memories]
|
|
198
|
+
|
|
199
|
+
result = self._extract_json(response)
|
|
200
|
+
|
|
201
|
+
if result and "operations" in result:
|
|
202
|
+
logger.info(f"[智能记忆] 决策 {len(result['operations'])} 个操作")
|
|
203
|
+
return result["operations"]
|
|
204
|
+
|
|
205
|
+
# 降级:全部 ADD
|
|
206
|
+
return [{"action": "ADD", "memory": mem} for mem in new_memories]
|
|
207
|
+
|
|
208
|
+
def execute_operations(self, operations: List[Dict]) -> Dict:
|
|
209
|
+
"""
|
|
210
|
+
执行记忆操作
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
执行结果
|
|
214
|
+
"""
|
|
215
|
+
results = {
|
|
216
|
+
"added": 0,
|
|
217
|
+
"updated": 0,
|
|
218
|
+
"deleted": 0,
|
|
219
|
+
"skipped": 0,
|
|
220
|
+
"errors": []
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
decay_manager = self._get_decay_manager()
|
|
224
|
+
|
|
225
|
+
for op in operations:
|
|
226
|
+
action = op.get("action", "NOOP")
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
if action == "ADD":
|
|
230
|
+
memory = op.get("memory", {})
|
|
231
|
+
content = memory.get("content", "")
|
|
232
|
+
importance = memory.get("importance", 0.5)
|
|
233
|
+
mem_type = memory.get("type", "fact")
|
|
234
|
+
|
|
235
|
+
if content:
|
|
236
|
+
memory_id = f"mem_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
|
237
|
+
decay_manager.register_memory(
|
|
238
|
+
memory_id=memory_id,
|
|
239
|
+
content=content,
|
|
240
|
+
importance=importance,
|
|
241
|
+
memory_type=mem_type
|
|
242
|
+
)
|
|
243
|
+
results["added"] += 1
|
|
244
|
+
|
|
245
|
+
elif action == "UPDATE":
|
|
246
|
+
memory_id = op.get("memory_id", "")
|
|
247
|
+
new_content = op.get("new_content", "")
|
|
248
|
+
|
|
249
|
+
if memory_id and new_content:
|
|
250
|
+
if memory_id in decay_manager.memories:
|
|
251
|
+
decay_manager.memories[memory_id].content = new_content
|
|
252
|
+
results["updated"] += 1
|
|
253
|
+
|
|
254
|
+
elif action == "DELETE":
|
|
255
|
+
memory_id = op.get("memory_id", "")
|
|
256
|
+
|
|
257
|
+
if memory_id and memory_id in decay_manager.memories:
|
|
258
|
+
decay_manager.memories[memory_id].archived = True
|
|
259
|
+
results["deleted"] += 1
|
|
260
|
+
|
|
261
|
+
elif action == "NOOP":
|
|
262
|
+
results["skipped"] += 1
|
|
263
|
+
|
|
264
|
+
except Exception as e:
|
|
265
|
+
results["errors"].append(str(e))
|
|
266
|
+
|
|
267
|
+
logger.info(f"[智能记忆] 执行完成: +{results['added']} ~{results['updated']} -{results['deleted']}")
|
|
268
|
+
|
|
269
|
+
return results
|
|
270
|
+
|
|
271
|
+
def process_conversation(self, conversation: str,
|
|
272
|
+
context: str = "") -> Dict:
|
|
273
|
+
"""
|
|
274
|
+
处理对话:提取 + 决策 + 执行
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
conversation: 对话内容
|
|
278
|
+
context: 额外上下文
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
处理结果
|
|
282
|
+
"""
|
|
283
|
+
# Phase 1: 提取
|
|
284
|
+
new_memories = self.extract_memories(conversation, context)
|
|
285
|
+
|
|
286
|
+
if not new_memories:
|
|
287
|
+
return {"status": "no_memories", "extracted": 0}
|
|
288
|
+
|
|
289
|
+
# Phase 2: 检索相关记忆
|
|
290
|
+
decay_manager = self._get_decay_manager()
|
|
291
|
+
existing_memories = []
|
|
292
|
+
|
|
293
|
+
for mem in new_memories:
|
|
294
|
+
content = mem.get("content", "")
|
|
295
|
+
for mid, m in decay_manager.memories.items():
|
|
296
|
+
if m.archived:
|
|
297
|
+
continue
|
|
298
|
+
existing_content = m.content
|
|
299
|
+
# 简单关键词匹配
|
|
300
|
+
if any(word in existing_content for word in content.split()[:3] if len(word) > 2):
|
|
301
|
+
existing_memories.append({"id": mid, **m.to_dict()})
|
|
302
|
+
|
|
303
|
+
# Phase 3: 决策
|
|
304
|
+
operations = self.decide_operations(new_memories, existing_memories)
|
|
305
|
+
|
|
306
|
+
# Phase 4: 执行
|
|
307
|
+
results = self.execute_operations(operations)
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
"status": "ok",
|
|
311
|
+
"extracted": len(new_memories),
|
|
312
|
+
"existing_checked": len(existing_memories),
|
|
313
|
+
"operations": results
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# ==================== 全局实例 ====================
|
|
318
|
+
|
|
319
|
+
_manager_instance = None
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def get_intelligent_manager() -> IntelligentMemoryManager:
|
|
323
|
+
"""获取智能记忆管理器实例"""
|
|
324
|
+
global _manager_instance
|
|
325
|
+
if _manager_instance is None:
|
|
326
|
+
_manager_instance = IntelligentMemoryManager()
|
|
327
|
+
return _manager_instance
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
# ==================== 便捷函数 ====================
|
|
331
|
+
|
|
332
|
+
def smart_memorize(conversation: str, context: str = "") -> Dict:
|
|
333
|
+
"""
|
|
334
|
+
智能记忆:自动提取 + 决策 + 存储
|
|
335
|
+
|
|
336
|
+
替代手动 memorize() 调用
|
|
337
|
+
"""
|
|
338
|
+
manager = get_intelligent_manager()
|
|
339
|
+
return manager.process_conversation(conversation, context)
|