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,304 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MindForge v5.0 多模态记忆模块
|
|
3
|
+
支持文本、图像、音频、代码、结构化数据
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import base64
|
|
7
|
+
import json
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Dict, List, Optional, Any, Tuple
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MultimodalType(Enum):
|
|
14
|
+
"""多模态类型"""
|
|
15
|
+
TEXT = "text"
|
|
16
|
+
IMAGE = "image"
|
|
17
|
+
AUDIO = "audio"
|
|
18
|
+
CODE = "code"
|
|
19
|
+
STRUCTURED = "structured"
|
|
20
|
+
MULTIMODAL = "multimodal"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class MultimodalContent:
|
|
25
|
+
"""多模态内容"""
|
|
26
|
+
content_type: MultimodalType
|
|
27
|
+
data: str = ""
|
|
28
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
29
|
+
embedding: Optional[List[float]] = None
|
|
30
|
+
text_representation: str = ""
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> dict:
|
|
33
|
+
return {
|
|
34
|
+
"content_type": self.content_type.value,
|
|
35
|
+
"data": self.data,
|
|
36
|
+
"metadata": self.metadata,
|
|
37
|
+
"text_representation": self.text_representation,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def from_dict(cls, data: dict) -> "MultimodalContent":
|
|
42
|
+
return cls(
|
|
43
|
+
content_type=MultimodalType(data.get("content_type", "text")),
|
|
44
|
+
data=data.get("data", ""),
|
|
45
|
+
metadata=data.get("metadata", {}),
|
|
46
|
+
text_representation=data.get("text_representation", ""),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class ImageMetadata:
|
|
52
|
+
"""图像元数据"""
|
|
53
|
+
width: int = 0
|
|
54
|
+
height: int = 0
|
|
55
|
+
format: str = ""
|
|
56
|
+
size_bytes: int = 0
|
|
57
|
+
caption: str = ""
|
|
58
|
+
objects_detected: List[str] = field(default_factory=list)
|
|
59
|
+
scene_description: str = ""
|
|
60
|
+
|
|
61
|
+
def to_dict(self) -> dict:
|
|
62
|
+
return self.__dict__
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class AudioMetadata:
|
|
67
|
+
"""音频元数据"""
|
|
68
|
+
duration_seconds: float = 0.0
|
|
69
|
+
format: str = ""
|
|
70
|
+
sample_rate: int = 0
|
|
71
|
+
transcript: str = ""
|
|
72
|
+
speaker: str = ""
|
|
73
|
+
language: str = ""
|
|
74
|
+
|
|
75
|
+
def to_dict(self) -> dict:
|
|
76
|
+
return self.__dict__
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class CodeMetadata:
|
|
81
|
+
"""代码元数据"""
|
|
82
|
+
language: str = ""
|
|
83
|
+
lines_of_code: int = 0
|
|
84
|
+
function_count: int = 0
|
|
85
|
+
class_count: int = 0
|
|
86
|
+
purpose: str = ""
|
|
87
|
+
dependencies: List[str] = field(default_factory=list)
|
|
88
|
+
|
|
89
|
+
def to_dict(self) -> dict:
|
|
90
|
+
return self.__dict__
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class MultimodalMemory:
|
|
94
|
+
"""多模态记忆管理器"""
|
|
95
|
+
|
|
96
|
+
def __init__(self, storage=None, index=None):
|
|
97
|
+
self.storage = storage
|
|
98
|
+
self.index = index
|
|
99
|
+
self._type_stats: Dict[str, int] = {}
|
|
100
|
+
|
|
101
|
+
def create_text_memory(self, text: str, **kwargs) -> MultimodalContent:
|
|
102
|
+
"""创建文本记忆"""
|
|
103
|
+
return MultimodalContent(
|
|
104
|
+
content_type=MultimodalType.TEXT,
|
|
105
|
+
data=text,
|
|
106
|
+
text_representation=text,
|
|
107
|
+
metadata={"word_count": len(text.split()), "char_count": len(text)},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def create_image_memory(self,
|
|
111
|
+
image_data: bytes,
|
|
112
|
+
caption: str = "",
|
|
113
|
+
width: int = 0,
|
|
114
|
+
height: int = 0,
|
|
115
|
+
format: str = "",
|
|
116
|
+
objects: Optional[List[str]] = None) -> MultimodalContent:
|
|
117
|
+
"""创建图像记忆"""
|
|
118
|
+
# v5.4.7 修复 L-9:限制图像大小(默认 50MB)
|
|
119
|
+
MAX_IMAGE_SIZE = 50 * 1024 * 1024
|
|
120
|
+
if len(image_data) > MAX_IMAGE_SIZE:
|
|
121
|
+
raise ValueError(f"Image too large: {len(image_data)} bytes (max {MAX_IMAGE_SIZE})")
|
|
122
|
+
encoded = base64.b64encode(image_data).decode()
|
|
123
|
+
metadata = ImageMetadata(
|
|
124
|
+
width=width,
|
|
125
|
+
height=height,
|
|
126
|
+
format=format,
|
|
127
|
+
size_bytes=len(image_data),
|
|
128
|
+
caption=caption,
|
|
129
|
+
objects_detected=objects or [],
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
text_repr = f"[图像] {caption} "
|
|
133
|
+
if objects:
|
|
134
|
+
text_repr += f"包含:{', '.join(objects)}"
|
|
135
|
+
|
|
136
|
+
return MultimodalContent(
|
|
137
|
+
content_type=MultimodalType.IMAGE,
|
|
138
|
+
data=encoded,
|
|
139
|
+
metadata=metadata.to_dict(),
|
|
140
|
+
text_representation=text_repr,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def create_audio_memory(self,
|
|
144
|
+
audio_data: bytes,
|
|
145
|
+
transcript: str = "",
|
|
146
|
+
duration: float = 0.0,
|
|
147
|
+
format: str = "",
|
|
148
|
+
speaker: str = "",
|
|
149
|
+
language: str = "") -> MultimodalContent:
|
|
150
|
+
"""创建音频记忆"""
|
|
151
|
+
# v5.4.7 修复 L-9:限制音频大小(默认 100MB)
|
|
152
|
+
MAX_AUDIO_SIZE = 100 * 1024 * 1024
|
|
153
|
+
if len(audio_data) > MAX_AUDIO_SIZE:
|
|
154
|
+
raise ValueError(f"Audio too large: {len(audio_data)} bytes (max {MAX_AUDIO_SIZE})")
|
|
155
|
+
encoded = base64.b64encode(audio_data).decode()
|
|
156
|
+
metadata = AudioMetadata(
|
|
157
|
+
duration_seconds=duration,
|
|
158
|
+
format=format,
|
|
159
|
+
transcript=transcript,
|
|
160
|
+
speaker=speaker,
|
|
161
|
+
language=language,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
text_repr = f"[音频] {speaker} 说:{transcript}" if transcript else "[音频]"
|
|
165
|
+
|
|
166
|
+
return MultimodalContent(
|
|
167
|
+
content_type=MultimodalType.AUDIO,
|
|
168
|
+
data=encoded,
|
|
169
|
+
metadata=metadata.to_dict(),
|
|
170
|
+
text_representation=text_repr,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def create_code_memory(self,
|
|
174
|
+
code: str,
|
|
175
|
+
language: str = "",
|
|
176
|
+
purpose: str = "",
|
|
177
|
+
dependencies: Optional[List[str]] = None) -> MultimodalContent:
|
|
178
|
+
"""创建代码记忆"""
|
|
179
|
+
lines = code.strip().split('\n')
|
|
180
|
+
func_count = sum(1 for line in lines if line.strip().startswith('def ') or line.strip().startswith('function '))
|
|
181
|
+
class_count = sum(1 for line in lines if line.strip().startswith('class '))
|
|
182
|
+
|
|
183
|
+
metadata = CodeMetadata(
|
|
184
|
+
language=language,
|
|
185
|
+
lines_of_code=len(lines),
|
|
186
|
+
function_count=func_count,
|
|
187
|
+
class_count=class_count,
|
|
188
|
+
purpose=purpose,
|
|
189
|
+
dependencies=dependencies or [],
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
text_repr = f"[代码-{language}] {purpose}\n{code[:200]}"
|
|
193
|
+
|
|
194
|
+
return MultimodalContent(
|
|
195
|
+
content_type=MultimodalType.CODE,
|
|
196
|
+
data=code,
|
|
197
|
+
metadata=metadata.to_dict(),
|
|
198
|
+
text_representation=text_repr,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def create_structured_memory(self,
|
|
202
|
+
data: Dict,
|
|
203
|
+
schema_name: str = "",
|
|
204
|
+
description: str = "") -> MultimodalContent:
|
|
205
|
+
"""创建结构化数据记忆"""
|
|
206
|
+
json_str = json.dumps(data, ensure_ascii=False, indent=2)
|
|
207
|
+
text_repr = f"[结构化数据-{schema_name}] {description}\n键:{', '.join(data.keys())}"
|
|
208
|
+
|
|
209
|
+
return MultimodalContent(
|
|
210
|
+
content_type=MultimodalType.STRUCTURED,
|
|
211
|
+
data=json_str,
|
|
212
|
+
metadata={"schema": schema_name, "key_count": len(data)},
|
|
213
|
+
text_representation=text_repr,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def create_multimodal_memory(self,
|
|
217
|
+
contents: List[MultimodalContent],
|
|
218
|
+
description: str = "") -> MultimodalContent:
|
|
219
|
+
"""创建复合多模态记忆"""
|
|
220
|
+
all_text = "\n".join(c.text_representation for c in contents)
|
|
221
|
+
combined_data = json.dumps([c.to_dict() for c in contents], ensure_ascii=False)
|
|
222
|
+
|
|
223
|
+
return MultimodalContent(
|
|
224
|
+
content_type=MultimodalType.MULTIMODAL,
|
|
225
|
+
data=combined_data,
|
|
226
|
+
metadata={
|
|
227
|
+
"modality_count": len(contents),
|
|
228
|
+
"modalities": [c.content_type.value for c in contents],
|
|
229
|
+
"description": description,
|
|
230
|
+
},
|
|
231
|
+
text_representation=all_text,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def search_multimodal(self,
|
|
235
|
+
query: str,
|
|
236
|
+
content_types: Optional[List[MultimodalType]] = None,
|
|
237
|
+
limit: int = 10) -> List[Tuple[MultimodalContent, float]]:
|
|
238
|
+
"""跨模态搜索
|
|
239
|
+
|
|
240
|
+
v6.1.2 修复(两个方向都会出错):`entry.memory_type` 是 MemoryType **枚举**,
|
|
241
|
+
原代码却拿它和 `[t.value for t in content_types]` 这个**字符串列表**比对——
|
|
242
|
+
枚举永远不等于字符串,所以只要传了 content_types,命中就全被丢弃、返回空;
|
|
243
|
+
而不传 content_types 时又走到 `MultimodalType(mtype)`,用枚举对象构造(应为
|
|
244
|
+
str),抛 ValueError。即「带过滤 = 静默漏检,不带过滤 = 直接崩溃」。
|
|
245
|
+
现在两侧统一归一到 .value 再比较,类型名无法识别时按 TEXT 暴露而非丢结果。
|
|
246
|
+
"""
|
|
247
|
+
if not self.index:
|
|
248
|
+
return []
|
|
249
|
+
|
|
250
|
+
results = self.index.search(query, top_k=limit * 2)
|
|
251
|
+
|
|
252
|
+
# 归一过滤集合:既接受 MultimodalType 枚举,也接受裸字符串值
|
|
253
|
+
allowed = None
|
|
254
|
+
if content_types:
|
|
255
|
+
allowed = {getattr(t, "value", t) for t in content_types}
|
|
256
|
+
|
|
257
|
+
filtered = []
|
|
258
|
+
for doc_id, score in results:
|
|
259
|
+
if self.storage:
|
|
260
|
+
entry = self.storage.get_memory(doc_id)
|
|
261
|
+
if entry:
|
|
262
|
+
mtype = getattr(entry, "memory_type", None)
|
|
263
|
+
# 兼容 MemoryType 枚举与裸字符串两种落库形态
|
|
264
|
+
type_value = getattr(mtype, "value", mtype)
|
|
265
|
+
if allowed is not None and type_value not in allowed:
|
|
266
|
+
continue
|
|
267
|
+
try:
|
|
268
|
+
ctype = MultimodalType(type_value)
|
|
269
|
+
except ValueError:
|
|
270
|
+
# 未知/缺失类型:按文本暴露,不让整条结果消失
|
|
271
|
+
ctype = MultimodalType.TEXT
|
|
272
|
+
content = MultimodalContent(
|
|
273
|
+
content_type=ctype,
|
|
274
|
+
data=entry.content,
|
|
275
|
+
text_representation=entry.content,
|
|
276
|
+
)
|
|
277
|
+
filtered.append((content, score))
|
|
278
|
+
|
|
279
|
+
return filtered[:limit]
|
|
280
|
+
|
|
281
|
+
def get_type_stats(self) -> Dict[str, int]:
|
|
282
|
+
"""获取各类型统计"""
|
|
283
|
+
if not self.storage:
|
|
284
|
+
return self._type_stats
|
|
285
|
+
|
|
286
|
+
stats = self.storage.get_stats()
|
|
287
|
+
return stats.get("by_type", {})
|
|
288
|
+
|
|
289
|
+
@staticmethod
|
|
290
|
+
def compute_similarity(content1: MultimodalContent, content2: MultimodalContent) -> float:
|
|
291
|
+
"""计算两个多模态内容的相似度"""
|
|
292
|
+
text1 = content1.text_representation.lower()
|
|
293
|
+
text2 = content2.text_representation.lower()
|
|
294
|
+
|
|
295
|
+
words1 = set(text1.split())
|
|
296
|
+
words2 = set(text2.split())
|
|
297
|
+
|
|
298
|
+
if not words1 or not words2:
|
|
299
|
+
return 0.0
|
|
300
|
+
|
|
301
|
+
intersection = words1 & words2
|
|
302
|
+
union = words1 | words2
|
|
303
|
+
|
|
304
|
+
return len(intersection) / len(union)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MindForge v6.0.0 功能 5:按结果学习的记忆
|
|
3
|
+
|
|
4
|
+
不止记"发生过什么",还要记"哪段记忆带来了好结果":
|
|
5
|
+
- 成功的方法 → 记忆重要度上调,检索时优先
|
|
6
|
+
- 失败的方法 → 自动降权
|
|
7
|
+
- 可在记忆中标注 outcome 并给出解释
|
|
8
|
+
|
|
9
|
+
实现:
|
|
10
|
+
- memories 表新增 outcome / outcome_note / outcome_at 三列(migration)
|
|
11
|
+
- apply_outcome(): 更新记忆的结果标记并调整 importance
|
|
12
|
+
- search_weighted(): 检索时按 outcome 加权(success 优先)
|
|
13
|
+
- outcome_stats(): 结果分布统计
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import time
|
|
19
|
+
from typing import Any, Dict, List, Optional
|
|
20
|
+
|
|
21
|
+
_OUTCOME_IMPORTANCE_MAP = {
|
|
22
|
+
"success": {"HIGH": 0, "MEDIUM": 1, "LOW": 1}, # 成功 → 上调
|
|
23
|
+
"partial": {"HIGH": 0, "MEDIUM": 0, "LOW": 0},
|
|
24
|
+
"failure": {"HIGH": 0, "MEDIUM": -1, "LOW": -1}, # 失败 → 下调
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_IMPORTANCE_ORDER = ["LOW", "MEDIUM", "HIGH"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _shift_importance(importance: str, outcome: str) -> str:
|
|
31
|
+
"""按结果调整重要度档位"""
|
|
32
|
+
imp = (importance or "MEDIUM").upper()
|
|
33
|
+
if imp not in _IMPORTANCE_ORDER:
|
|
34
|
+
imp = "MEDIUM"
|
|
35
|
+
shift = _OUTCOME_IMPORTANCE_MAP.get(outcome, {}).get(imp, 0)
|
|
36
|
+
idx = _IMPORTANCE_ORDER.index(imp)
|
|
37
|
+
idx = max(0, min(len(_IMPORTANCE_ORDER) - 1, idx + shift))
|
|
38
|
+
return _IMPORTANCE_ORDER[idx]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _ensure_columns(conn) -> None:
|
|
42
|
+
"""幂等添加 outcome 列"""
|
|
43
|
+
for col, ddl in (
|
|
44
|
+
("outcome", "TEXT DEFAULT ''"),
|
|
45
|
+
("outcome_note", "TEXT DEFAULT ''"),
|
|
46
|
+
("outcome_at", "REAL DEFAULT 0"),
|
|
47
|
+
):
|
|
48
|
+
try:
|
|
49
|
+
conn.execute(f"ALTER TABLE memories ADD COLUMN {col} {ddl}")
|
|
50
|
+
except Exception:
|
|
51
|
+
pass # 已存在
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _normalize_memory_id(memory_id: Any) -> tuple:
|
|
55
|
+
"""接受 str 或 MemoryEntry 对象,返回 (id_or_None, error_or_None)。
|
|
56
|
+
|
|
57
|
+
v6.0.0 只接受字符串;但 mf.add() 返回的是 MemoryEntry 对象,
|
|
58
|
+
把返回值直接传进来会在 len() 处抛 TypeError(对外表现为崩溃而非友好报错)。
|
|
59
|
+
v6.1.1 起统一收敛:对象取 .id,其余非法类型返回明确错误。
|
|
60
|
+
"""
|
|
61
|
+
if memory_id is None:
|
|
62
|
+
return None, "记忆 ID 无效"
|
|
63
|
+
if not isinstance(memory_id, str):
|
|
64
|
+
memory_id = getattr(memory_id, "id", None)
|
|
65
|
+
if not isinstance(memory_id, str):
|
|
66
|
+
return None, "记忆 ID 必须是字符串或 add() 返回的 MemoryEntry 对象"
|
|
67
|
+
if not memory_id or len(memory_id) > 128:
|
|
68
|
+
return None, "记忆 ID 无效"
|
|
69
|
+
return memory_id, None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _chunk_score(chunk: Any) -> float:
|
|
73
|
+
"""读取召回得分。
|
|
74
|
+
|
|
75
|
+
RecallEngine 写入的字段名是 relevance_score(见 modules/recall.py),
|
|
76
|
+
v6.0.0 的 search_weighted 误读为 score,getattr 取不到就落回 0.0,
|
|
77
|
+
导致按结果加权检索返回的 relevance 恒为 0.0。
|
|
78
|
+
"""
|
|
79
|
+
for attr in ("relevance_score", "score", "similarity"):
|
|
80
|
+
val = getattr(chunk, attr, None)
|
|
81
|
+
if val is not None:
|
|
82
|
+
try:
|
|
83
|
+
return float(val)
|
|
84
|
+
except (TypeError, ValueError):
|
|
85
|
+
continue
|
|
86
|
+
if isinstance(chunk, dict):
|
|
87
|
+
for key in ("relevance_score", "score", "similarity"):
|
|
88
|
+
if chunk.get(key) is not None:
|
|
89
|
+
try:
|
|
90
|
+
return float(chunk[key])
|
|
91
|
+
except (TypeError, ValueError):
|
|
92
|
+
continue
|
|
93
|
+
return 0.0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def apply_outcome(storage, memory_id: str, outcome: str = "success",
|
|
97
|
+
note: str = "", actor: str = "",
|
|
98
|
+
session_id: str = "") -> Dict[str, Any]:
|
|
99
|
+
"""给记忆标记结果并调整重要度。outcome: success/partial/failure"""
|
|
100
|
+
outcome = (outcome or "success").lower()
|
|
101
|
+
if outcome not in ("success", "partial", "failure"):
|
|
102
|
+
return {"success": False, "error": f"无效 outcome: {outcome}"}
|
|
103
|
+
memory_id, id_error = _normalize_memory_id(memory_id)
|
|
104
|
+
if id_error:
|
|
105
|
+
return {"success": False, "error": id_error}
|
|
106
|
+
|
|
107
|
+
conn = storage._get_conn()
|
|
108
|
+
_ensure_columns(conn)
|
|
109
|
+
row = conn.execute(
|
|
110
|
+
"SELECT importance, outcome FROM memories WHERE id = ?", (memory_id,)).fetchone()
|
|
111
|
+
if not row:
|
|
112
|
+
return {"success": False, "error": "记忆不存在"}
|
|
113
|
+
old_importance, old_outcome = row[0] or "MEDIUM", row[1] or ""
|
|
114
|
+
new_importance = _shift_importance(old_importance, outcome)
|
|
115
|
+
|
|
116
|
+
now = time.time()
|
|
117
|
+
conn.execute(
|
|
118
|
+
"UPDATE memories SET outcome = ?, outcome_note = ?, outcome_at = ?, "
|
|
119
|
+
"importance = ? WHERE id = ?",
|
|
120
|
+
(outcome, (note or "")[:500], now, new_importance, memory_id))
|
|
121
|
+
conn.commit()
|
|
122
|
+
storage._add_audit("outcome_learn", memory_id, actor or "", session_id or "",
|
|
123
|
+
"", {"outcome": outcome, "importance": new_importance,
|
|
124
|
+
"note": (note or "")[:200]})
|
|
125
|
+
return {"success": True, "memory_id": memory_id, "outcome": outcome,
|
|
126
|
+
"importance_before": old_importance, "importance_after": new_importance,
|
|
127
|
+
"outcome_at": now}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def outcome_stats(storage) -> Dict[str, Any]:
|
|
131
|
+
"""结果标记分布统计"""
|
|
132
|
+
conn = storage._get_conn()
|
|
133
|
+
_ensure_columns(conn)
|
|
134
|
+
try:
|
|
135
|
+
rows = conn.execute(
|
|
136
|
+
"SELECT outcome, COUNT(*) FROM memories WHERE outcome != '' "
|
|
137
|
+
"GROUP BY outcome").fetchall()
|
|
138
|
+
except Exception:
|
|
139
|
+
rows = []
|
|
140
|
+
dist = {r[0]: r[1] for r in rows}
|
|
141
|
+
total = sum(dist.values())
|
|
142
|
+
return {
|
|
143
|
+
"total_marked": total,
|
|
144
|
+
"distribution": dist,
|
|
145
|
+
"success_rate": round(dist.get("success", 0) / total, 3) if total else 0.0,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def search_weighted(mf, query: str, max_results: int = 10,
|
|
150
|
+
outcome: str = "success",
|
|
151
|
+
min_relevance: float = 0.0) -> List[Dict[str, Any]]:
|
|
152
|
+
"""按结果加权检索:先用正常检索召回,再把命中 outcome 的记忆提到前面。
|
|
153
|
+
|
|
154
|
+
返回 [{memory_id, content_preview, outcome, importance, relevance}...]
|
|
155
|
+
"""
|
|
156
|
+
from mindforge.modules.outcome_learning import _ensure_columns
|
|
157
|
+
|
|
158
|
+
results = mf.search(query=query, max_results=max(5, int(max_results) * 2),
|
|
159
|
+
min_relevance=float(min_relevance or 0.0))
|
|
160
|
+
chunks = getattr(results, "chunks", None) or (results if isinstance(results, list) else [])
|
|
161
|
+
out = []
|
|
162
|
+
for r in chunks:
|
|
163
|
+
mid = getattr(r, "memory_id", None) or getattr(r, "id", None)
|
|
164
|
+
if not mid:
|
|
165
|
+
continue
|
|
166
|
+
row = None
|
|
167
|
+
try:
|
|
168
|
+
conn = mf._storage._get_conn()
|
|
169
|
+
_ensure_columns(conn)
|
|
170
|
+
row = conn.execute(
|
|
171
|
+
"SELECT outcome, importance, content FROM memories WHERE id = ?",
|
|
172
|
+
(mid,)).fetchone()
|
|
173
|
+
except Exception:
|
|
174
|
+
pass
|
|
175
|
+
out.append({
|
|
176
|
+
"memory_id": mid,
|
|
177
|
+
"content_preview": (getattr(r, "content", "") or "")[:200],
|
|
178
|
+
"outcome": row[0] if row else "",
|
|
179
|
+
"importance": row[1] if row else "",
|
|
180
|
+
"relevance": round(_chunk_score(r), 4),
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
# 目标 outcome 的记忆前置
|
|
184
|
+
out.sort(key=lambda x: (0 if x["outcome"] == outcome else 1,
|
|
185
|
+
-x["relevance"]))
|
|
186
|
+
return out[:max(1, int(max_results))]
|