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,340 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MindForge 记忆 → 技能转化引擎
|
|
3
|
+
====================================
|
|
4
|
+
核心想法:一组「解决某问题」的记忆,可以被抽象成可复用的技能模板:
|
|
5
|
+
Skill = {name, description, triggers, slots, steps, examples, tags}
|
|
6
|
+
|
|
7
|
+
抽取流程:
|
|
8
|
+
1. 候选聚类:按标签 + 关键词 overlap 把记忆分桶(同一技能 ≈ 主题簇)
|
|
9
|
+
2. 槽位识别:在每条记忆里找出 {{参数}} / <参数> / 变量名模式
|
|
10
|
+
3. 步骤提炼:按「先/然后/最后/接下来/步骤 1~n」等连接词 + 序号模式抽取步骤序列
|
|
11
|
+
4. 触发词归纳:出现频次 ≥ min_hits 的关键词/标签 即为触发词
|
|
12
|
+
5. 示例采样:从簇中挑最多 k 条作为 skill 的 examples
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# 模式定义
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
_SLOT_PATTERNS = [
|
|
26
|
+
re.compile(r"\{\{\s*([A-Za-z_][A-Za-z0-9_]{0,29})\s*\}\}"),
|
|
27
|
+
re.compile(r"<\s*([A-Za-z_][A-Za-z0-9_]{0,29})\s*>"),
|
|
28
|
+
re.compile(r"(\$[A-Z_][A-Z0-9_]{0,19})"),
|
|
29
|
+
re.compile(r"[「『](变量|参数|占位|入参)?\s*([A-Za-z_][A-Za-z0-9_\u4e00-\u9fa5]{1,15})\s*[」』]"),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
_STEP_PATTERNS = [
|
|
33
|
+
(re.compile(r"(?:步骤|第)\s*([0-9一二三四五六七八九十]{1,4})\s*[、步::\.]\s*([^\n,,。;;]{2,120})"), "numbered"),
|
|
34
|
+
(re.compile(r"(首先|第一步|先|一开始)[::\s]*([^\n,,。;;]{2,120})"), "first"),
|
|
35
|
+
(re.compile(r"(然后|接着|接下来|下一步|第二步)[::\s]*([^\n,,。;;]{2,120})"), "next"),
|
|
36
|
+
(re.compile(r"(最后|最终|完成后|收尾|第三步)[::\s]*([^\n,,。;;]{2,120})"), "last"),
|
|
37
|
+
(re.compile(r"(注意|提示|警告|重要)[::\s]*([^\n,,。;;]{2,120})"), "note"),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
_CMD_VERBS = ["安装", "配置", "启动", "停止", "运行", "执行", "调用", "发送", "请求",
|
|
41
|
+
"打开", "访问", "下载", "上传", "生成", "创建", "删除", "修改", "更新",
|
|
42
|
+
"导出", "导入", "备份", "恢复", "部署", "测试", "验证", "构建", "打包"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# 数据类
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class SkillSlot:
|
|
51
|
+
name: str
|
|
52
|
+
examples: List[str] = field(default_factory=list)
|
|
53
|
+
required: bool = False
|
|
54
|
+
default: Optional[str] = None
|
|
55
|
+
|
|
56
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
57
|
+
return {"name": self.name, "examples": self.examples[:5],
|
|
58
|
+
"required": self.required, "default": self.default}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class SkillTemplate:
|
|
63
|
+
name: str
|
|
64
|
+
description: str
|
|
65
|
+
triggers: List[str] = field(default_factory=list)
|
|
66
|
+
slots: List[SkillSlot] = field(default_factory=list)
|
|
67
|
+
steps: List[Dict[str, Any]] = field(default_factory=list)
|
|
68
|
+
examples: List[str] = field(default_factory=list)
|
|
69
|
+
tags: List[str] = field(default_factory=list)
|
|
70
|
+
source_memory_ids: List[str] = field(default_factory=list)
|
|
71
|
+
confidence: float = 0.0
|
|
72
|
+
cluster_size: int = 0
|
|
73
|
+
|
|
74
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
75
|
+
return {
|
|
76
|
+
"name": self.name,
|
|
77
|
+
"description": self.description,
|
|
78
|
+
"triggers": self.triggers[:20],
|
|
79
|
+
"slots": [s.to_dict() for s in self.slots],
|
|
80
|
+
"steps": self.steps[:30],
|
|
81
|
+
"examples": self.examples[:5],
|
|
82
|
+
"tags": self.tags[:20],
|
|
83
|
+
"source_memory_ids": self.source_memory_ids[:100],
|
|
84
|
+
"confidence": round(self.confidence, 3),
|
|
85
|
+
"cluster_size": self.cluster_size,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
def render(self, **kwargs) -> str:
|
|
89
|
+
"""把技能模板用用户提供的 slot 值填回去,得到可执行流程文本。"""
|
|
90
|
+
mapping = {s.name: kwargs.get(s.name, s.default or f"{{{{{s.name}}}}}")
|
|
91
|
+
for s in self.slots}
|
|
92
|
+
lines = [f"# 技能:{self.name}"]
|
|
93
|
+
if self.description:
|
|
94
|
+
lines.append(f"> {self.description}")
|
|
95
|
+
lines.append("")
|
|
96
|
+
if self.steps:
|
|
97
|
+
lines.append("## 步骤")
|
|
98
|
+
for i, step in enumerate(self.steps, 1):
|
|
99
|
+
action = str(step.get("action", ""))
|
|
100
|
+
for k, v in mapping.items():
|
|
101
|
+
action = re.sub(rf"(\{{{{\s*{re.escape(k)}\s*\}}}}|<\s*{re.escape(k)}\s*>)", str(v), action)
|
|
102
|
+
lines.append(f"{i}. {action}")
|
|
103
|
+
if self.examples:
|
|
104
|
+
lines.append("")
|
|
105
|
+
lines.append("## 示例")
|
|
106
|
+
for ex in self.examples:
|
|
107
|
+
lines.append(f"- {ex}")
|
|
108
|
+
return "\n".join(lines)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
# 引擎
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
class SkillExtractor:
|
|
116
|
+
"""无外部依赖的记忆→技能抽取引擎。"""
|
|
117
|
+
|
|
118
|
+
def __init__(self,
|
|
119
|
+
min_cluster_size: int = 2,
|
|
120
|
+
max_skills: int = 50,
|
|
121
|
+
overlap_threshold: float = 0.35, # v5.4.8 P3-002: 从 0.20 提高到 0.35
|
|
122
|
+
min_trigger_hits: int = 1,
|
|
123
|
+
max_step_per_skill: int = 25):
|
|
124
|
+
self.min_cluster_size = max(1, int(min_cluster_size))
|
|
125
|
+
self.max_skills = max_skills
|
|
126
|
+
self.base_threshold = overlap_threshold
|
|
127
|
+
self.overlap_threshold = overlap_threshold
|
|
128
|
+
self.min_trigger_hits = min_trigger_hits
|
|
129
|
+
self.max_step = max_step_per_skill
|
|
130
|
+
|
|
131
|
+
# -- helpers -----------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def _ngrams(text: str, n: int = 2) -> Set[str]:
|
|
135
|
+
s = re.sub(r"[\W_]+", "", text)
|
|
136
|
+
if len(s) < n:
|
|
137
|
+
return {s} if s else set()
|
|
138
|
+
return {s[i:i + n] for i in range(len(s) - n + 1)}
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def _tags_of(mem: Dict[str, Any]) -> List[str]:
|
|
142
|
+
raw = mem.get("tags") or []
|
|
143
|
+
if isinstance(raw, str):
|
|
144
|
+
return [t.strip() for t in re.split(r"[,,#\s]+", raw) if t.strip()]
|
|
145
|
+
return [str(t).strip() for t in raw if str(t).strip()]
|
|
146
|
+
|
|
147
|
+
def _cluster(self, memories: List[Dict[str, Any]]) -> List[List[int]]:
|
|
148
|
+
n = len(memories)
|
|
149
|
+
parent = list(range(n))
|
|
150
|
+
|
|
151
|
+
# v5.4.8 P3-002 修复:自适应阈值,防止大数据集下的链式合并
|
|
152
|
+
# 数据集越大,阈值越高,避免不相关的记忆通过中间节点连接
|
|
153
|
+
import math
|
|
154
|
+
adaptive_threshold = min(
|
|
155
|
+
self.base_threshold + 0.05 * math.log(max(n, 1)),
|
|
156
|
+
0.60 # 上限 0.60
|
|
157
|
+
)
|
|
158
|
+
threshold = max(self.base_threshold, adaptive_threshold)
|
|
159
|
+
|
|
160
|
+
def find(x: int) -> int:
|
|
161
|
+
while parent[x] != x:
|
|
162
|
+
parent[x] = parent[parent[x]]
|
|
163
|
+
x = parent[x]
|
|
164
|
+
return x
|
|
165
|
+
|
|
166
|
+
def union(a: int, b: int) -> None:
|
|
167
|
+
ra, rb = find(a), find(b)
|
|
168
|
+
if ra != rb:
|
|
169
|
+
parent[ra] = rb
|
|
170
|
+
|
|
171
|
+
feats: List[Tuple[Set[str], Set[str]]] = []
|
|
172
|
+
for m in memories:
|
|
173
|
+
content = str(m.get("content", ""))
|
|
174
|
+
feats.append((self._ngrams(content, 2), set(self._tags_of(m))))
|
|
175
|
+
|
|
176
|
+
for i in range(n):
|
|
177
|
+
ai, ti = feats[i]
|
|
178
|
+
for j in range(i + 1, n):
|
|
179
|
+
aj, tj = feats[j]
|
|
180
|
+
overlap_n = 0.0
|
|
181
|
+
if ai and aj:
|
|
182
|
+
overlap_n = len(ai & aj) / max(1, min(len(ai), len(aj)))
|
|
183
|
+
overlap_t = 0.0
|
|
184
|
+
if ti and tj:
|
|
185
|
+
overlap_t = len(ti & tj) / max(1, min(len(ti), len(tj)))
|
|
186
|
+
score = max(overlap_n, overlap_t)
|
|
187
|
+
# 若标签完全相同直接合并
|
|
188
|
+
if ti and tj and ti == tj:
|
|
189
|
+
score = max(score, 0.85)
|
|
190
|
+
if score >= threshold:
|
|
191
|
+
union(i, j)
|
|
192
|
+
|
|
193
|
+
buckets: Dict[int, List[int]] = {}
|
|
194
|
+
for i in range(n):
|
|
195
|
+
buckets.setdefault(find(i), []).append(i)
|
|
196
|
+
return [b for b in buckets.values() if len(b) >= self.min_cluster_size]
|
|
197
|
+
|
|
198
|
+
# -- extractors --------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
def _extract_slots(self, texts: List[str]) -> List[SkillSlot]:
|
|
201
|
+
slot_hits: Dict[str, Set[str]] = {}
|
|
202
|
+
for t in texts:
|
|
203
|
+
for pat in _SLOT_PATTERNS:
|
|
204
|
+
for m in pat.finditer(t):
|
|
205
|
+
name = m.group(1) if pat.groups == 1 else m.group(m.lastindex or 1)
|
|
206
|
+
# group count 不同,统一兜底
|
|
207
|
+
try:
|
|
208
|
+
if m.lastindex and m.lastindex >= 1:
|
|
209
|
+
name = m.group(m.lastindex)
|
|
210
|
+
except Exception:
|
|
211
|
+
pass
|
|
212
|
+
if not name or len(name) < 1:
|
|
213
|
+
continue
|
|
214
|
+
slot_hits.setdefault(name, set()).add(t[:60])
|
|
215
|
+
out = []
|
|
216
|
+
for name, exs in slot_hits.items():
|
|
217
|
+
out.append(SkillSlot(name=name, examples=list(exs), required=False))
|
|
218
|
+
out.sort(key=lambda s: -len(s.examples))
|
|
219
|
+
return out
|
|
220
|
+
|
|
221
|
+
def _extract_steps(self, texts: List[str]) -> List[Dict[str, Any]]:
|
|
222
|
+
steps: List[Tuple[int, str, str]] = [] # (order_key, action, kind)
|
|
223
|
+
for t in texts:
|
|
224
|
+
for pat, kind in _STEP_PATTERNS:
|
|
225
|
+
for m in pat.finditer(t):
|
|
226
|
+
raw_order = m.group(1)
|
|
227
|
+
action = m.group(2).strip(" \t")
|
|
228
|
+
order_key = 50
|
|
229
|
+
if kind == "first":
|
|
230
|
+
order_key = 10
|
|
231
|
+
elif kind == "next":
|
|
232
|
+
order_key = 30
|
|
233
|
+
elif kind == "last":
|
|
234
|
+
order_key = 70
|
|
235
|
+
elif kind == "note":
|
|
236
|
+
order_key = 90
|
|
237
|
+
elif kind == "numbered":
|
|
238
|
+
# 中文数字/阿拉伯数字 转 int,超出范围给个中值
|
|
239
|
+
try:
|
|
240
|
+
if raw_order.isdigit():
|
|
241
|
+
order_key = 10 + int(raw_order) * 2
|
|
242
|
+
else:
|
|
243
|
+
cn = "一二三四五六七八九十"
|
|
244
|
+
order_key = 10 + (cn.index(raw_order) + 1) * 2
|
|
245
|
+
except Exception:
|
|
246
|
+
order_key = 20
|
|
247
|
+
steps.append((order_key, action, kind))
|
|
248
|
+
# 去重 + 排序 + 截断
|
|
249
|
+
seen = set()
|
|
250
|
+
out = []
|
|
251
|
+
for key, action, kind in sorted(steps, key=lambda x: x[0]):
|
|
252
|
+
sig = action[:40]
|
|
253
|
+
if sig in seen:
|
|
254
|
+
continue
|
|
255
|
+
seen.add(sig)
|
|
256
|
+
out.append({"order": key, "action": action, "kind": kind})
|
|
257
|
+
if len(out) >= self.max_step:
|
|
258
|
+
break
|
|
259
|
+
return out
|
|
260
|
+
|
|
261
|
+
def _collect_triggers(self, memories_in_cluster: List[Dict[str, Any]],
|
|
262
|
+
texts: List[str]) -> List[str]:
|
|
263
|
+
freq: Dict[str, int] = {}
|
|
264
|
+
# 标签
|
|
265
|
+
for m in memories_in_cluster:
|
|
266
|
+
for t in self._tags_of(m):
|
|
267
|
+
freq[t] = freq.get(t, 0) + 1
|
|
268
|
+
# 命令动词
|
|
269
|
+
joined = "\n".join(texts)
|
|
270
|
+
for v in _CMD_VERBS:
|
|
271
|
+
c = joined.count(v)
|
|
272
|
+
if c > 0:
|
|
273
|
+
freq[v] = freq.get(v, 0) + c
|
|
274
|
+
# 2-gram 中高并集频(简化:取频次高的前若干)
|
|
275
|
+
items = [(k, v) for k, v in freq.items() if v >= self.min_trigger_hits]
|
|
276
|
+
items.sort(key=lambda kv: (-kv[1], kv[0]))
|
|
277
|
+
return [k for k, _ in items[:20]]
|
|
278
|
+
|
|
279
|
+
@staticmethod
|
|
280
|
+
def _summarize_name(memories: List[Dict[str, Any]]) -> Tuple[str, str]:
|
|
281
|
+
tags: Set[str] = set()
|
|
282
|
+
first_lines: List[str] = []
|
|
283
|
+
for m in memories:
|
|
284
|
+
for t in SkillExtractor._tags_of(m):
|
|
285
|
+
tags.add(t)
|
|
286
|
+
c = str(m.get("content", ""))
|
|
287
|
+
first_line = re.split(r"[\n,,。;;]", c, maxsplit=1)[0][:40].strip()
|
|
288
|
+
if first_line:
|
|
289
|
+
first_lines.append(first_line)
|
|
290
|
+
tags_list = sorted(tags)
|
|
291
|
+
name_prefix = "_".join(tags_list[:2]) if tags_list else "skill"
|
|
292
|
+
name = f"{name_prefix}_{len(memories)}mem".replace(" ", "_")
|
|
293
|
+
desc_parts = first_lines[:2]
|
|
294
|
+
description = ";".join(desc_parts) if desc_parts else f"从 {len(memories)} 条记忆归纳的通用流程"
|
|
295
|
+
return name, description
|
|
296
|
+
|
|
297
|
+
# -- main API ----------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
def extract(self, memories: List[Dict[str, Any]]) -> List[SkillTemplate]:
|
|
300
|
+
"""从一组记忆中批量抽取技能。"""
|
|
301
|
+
clusters = self._cluster(memories)
|
|
302
|
+
# 优先大簇
|
|
303
|
+
clusters.sort(key=lambda c: -len(c))
|
|
304
|
+
skills: List[SkillTemplate] = []
|
|
305
|
+
for cl in clusters[: self.max_skills]:
|
|
306
|
+
cl_mems = [memories[i] for i in cl]
|
|
307
|
+
texts = [str(m.get("content", "")) for m in cl_mems]
|
|
308
|
+
ids = [str(m.get("id", i)) for i, m in zip(cl, cl_mems)]
|
|
309
|
+
name, desc = self._summarize_name(cl_mems)
|
|
310
|
+
slots = self._extract_slots(texts)
|
|
311
|
+
steps = self._extract_steps(texts)
|
|
312
|
+
triggers = self._collect_triggers(cl_mems, texts)
|
|
313
|
+
confidence = 0.0
|
|
314
|
+
if triggers:
|
|
315
|
+
confidence += min(len(triggers) * 0.04, 0.35)
|
|
316
|
+
if slots:
|
|
317
|
+
confidence += min(len(slots) * 0.06, 0.25)
|
|
318
|
+
if steps:
|
|
319
|
+
confidence += min(len(steps) * 0.04, 0.30)
|
|
320
|
+
if len(cl) >= 4:
|
|
321
|
+
confidence += 0.10
|
|
322
|
+
examples = [t for t in texts if len(t) >= 8][:5]
|
|
323
|
+
# tags 汇总
|
|
324
|
+
tag_set: Set[str] = set()
|
|
325
|
+
for m in cl_mems:
|
|
326
|
+
for t in self._tags_of(m):
|
|
327
|
+
tag_set.add(t)
|
|
328
|
+
skills.append(SkillTemplate(
|
|
329
|
+
name=name, description=desc,
|
|
330
|
+
triggers=triggers, slots=slots, steps=steps,
|
|
331
|
+
examples=examples, tags=sorted(tag_set),
|
|
332
|
+
source_memory_ids=ids,
|
|
333
|
+
confidence=min(confidence, 1.0),
|
|
334
|
+
cluster_size=len(cl),
|
|
335
|
+
))
|
|
336
|
+
skills.sort(key=lambda s: (-s.confidence, -s.cluster_size))
|
|
337
|
+
return skills
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
__all__ = ["SkillExtractor", "SkillTemplate", "SkillSlot"]
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MindForge v6.0.0 功能 3:可分享的技能包
|
|
3
|
+
|
|
4
|
+
让用户把一组好用的技能模板一键导出为可分享文件,别人一键导入安装。
|
|
5
|
+
|
|
6
|
+
- PACK_FORMAT: "mindforge-skill-pack",FORMAT_VERSION = 1
|
|
7
|
+
- export_skill_pack(): 按技能名导出为结构化 JSON
|
|
8
|
+
- validate_skill_pack(): 校验格式/版本/条数上限,防恶意包
|
|
9
|
+
- import_skill_pack(): merge(按 name UPSERT 合并)或 replace(先删后写)
|
|
10
|
+
|
|
11
|
+
安全约束:
|
|
12
|
+
- MAX_SKILLS_PER_PACK = 500,MAX_CASES = 2000(防超大包)
|
|
13
|
+
- 字段白名单解析,忽略未知字段
|
|
14
|
+
- 导入失败原子回滚(单技能失败不影响已导入项)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import time
|
|
21
|
+
from typing import Any, Dict, List, Optional
|
|
22
|
+
|
|
23
|
+
from mindforge.modules.skill_extractor import SkillSlot, SkillTemplate
|
|
24
|
+
|
|
25
|
+
PACK_FORMAT = "mindforge-skill-pack"
|
|
26
|
+
FORMAT_VERSION = 1
|
|
27
|
+
|
|
28
|
+
MAX_SKILLS_PER_PACK = 500
|
|
29
|
+
MAX_CASES_PER_PACK = 2000
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def export_skill_pack(skill_store, names: Optional[List[str]] = None,
|
|
33
|
+
include_cases: bool = True,
|
|
34
|
+
max_cases: int = 200) -> Dict[str, Any]:
|
|
35
|
+
"""按技能名导出技能包(可分享文件内容)。"""
|
|
36
|
+
skills = skill_store.list_skills(limit=MAX_SKILLS_PER_PACK)
|
|
37
|
+
if names:
|
|
38
|
+
wanted = {n.lower() for n in names if n and n.strip()}
|
|
39
|
+
skills = [s for s in skills if (s.get("name") or "").lower() in wanted]
|
|
40
|
+
if not skills:
|
|
41
|
+
return {"success": False, "error": "没有可导出的技能"}
|
|
42
|
+
|
|
43
|
+
pack_skills = []
|
|
44
|
+
for s in skills[:MAX_SKILLS_PER_PACK]:
|
|
45
|
+
pack_skills.append({
|
|
46
|
+
"name": s.get("name", ""),
|
|
47
|
+
"description": s.get("description", ""),
|
|
48
|
+
"triggers": list(s.get("triggers", []) or [])[:20],
|
|
49
|
+
"slots": [{"name": sl.get("name", ""),
|
|
50
|
+
"examples": list(sl.get("examples", []) or [])[:5],
|
|
51
|
+
"required": bool(sl.get("required", False)),
|
|
52
|
+
"default": sl.get("default")}
|
|
53
|
+
for sl in (s.get("slots", []) or [])[:20]],
|
|
54
|
+
"steps": list(s.get("steps", []) or [])[:30],
|
|
55
|
+
"examples": list(s.get("examples", []) or [])[:5],
|
|
56
|
+
"tags": list(s.get("tags", []) or [])[:20],
|
|
57
|
+
"confidence": float(s.get("confidence", 0.0) or 0.0),
|
|
58
|
+
"cluster_size": int(s.get("cluster_size", 0) or 0),
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
pack = {
|
|
62
|
+
"format": PACK_FORMAT,
|
|
63
|
+
"format_version": FORMAT_VERSION,
|
|
64
|
+
"exported_at": round(time.time(), 3),
|
|
65
|
+
"skills": pack_skills,
|
|
66
|
+
}
|
|
67
|
+
if include_cases:
|
|
68
|
+
cases = skill_store.list_cases(limit=min(max_cases, MAX_CASES_PER_PACK))
|
|
69
|
+
pack["cases"] = [{
|
|
70
|
+
"task": c.get("task", ""),
|
|
71
|
+
"content": (c.get("content") or "")[:2000],
|
|
72
|
+
"result": (c.get("result") or "")[:2000],
|
|
73
|
+
"outcome": c.get("outcome", "success"),
|
|
74
|
+
"duration_seconds": float(c.get("duration_seconds", 0.0) or 0.0),
|
|
75
|
+
"tags": list(c.get("tags", []) or [])[:20],
|
|
76
|
+
} for c in cases[:MAX_CASES_PER_PACK]]
|
|
77
|
+
return {"success": True, "pack": pack,
|
|
78
|
+
"skill_count": len(pack_skills),
|
|
79
|
+
"case_count": len(pack.get("cases", []))}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def validate_skill_pack(pack: Any) -> Dict[str, Any]:
|
|
83
|
+
"""校验技能包格式。返回 {valid, errors}。"""
|
|
84
|
+
errors = []
|
|
85
|
+
if not isinstance(pack, dict):
|
|
86
|
+
return {"valid": False, "errors": ["技能包必须是 JSON 对象"]}
|
|
87
|
+
if pack.get("format") != PACK_FORMAT:
|
|
88
|
+
errors.append(f"格式标识错误: {pack.get('format')!r}(应为 {PACK_FORMAT})")
|
|
89
|
+
ver = pack.get("format_version")
|
|
90
|
+
if ver != FORMAT_VERSION:
|
|
91
|
+
errors.append(f"版本不支持: {ver!r}(当前支持 v{FORMAT_VERSION})")
|
|
92
|
+
skills = pack.get("skills")
|
|
93
|
+
if not isinstance(skills, list):
|
|
94
|
+
errors.append("缺少 skills 列表")
|
|
95
|
+
elif not skills:
|
|
96
|
+
errors.append("技能包为空(无技能)")
|
|
97
|
+
elif len(skills) > MAX_SKILLS_PER_PACK:
|
|
98
|
+
errors.append(f"技能数超过上限 {MAX_SKILLS_PER_PACK}")
|
|
99
|
+
else:
|
|
100
|
+
names = set()
|
|
101
|
+
for i, s in enumerate(skills):
|
|
102
|
+
if not isinstance(s, dict):
|
|
103
|
+
errors.append(f"skills[{i}] 不是对象")
|
|
104
|
+
continue
|
|
105
|
+
name = (s.get("name") or "").strip()
|
|
106
|
+
if not name:
|
|
107
|
+
errors.append(f"skills[{i}] 缺少 name")
|
|
108
|
+
elif name.lower() in names:
|
|
109
|
+
errors.append(f"技能名重复: {name}")
|
|
110
|
+
names.add(name.lower())
|
|
111
|
+
cases = pack.get("cases")
|
|
112
|
+
if isinstance(cases, list) and len(cases) > MAX_CASES_PER_PACK:
|
|
113
|
+
errors.append(f"案例数超过上限 {MAX_CASES_PER_PACK}")
|
|
114
|
+
return {"valid": not errors, "errors": errors}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _parse_skill(pack_skill: Dict[str, Any]) -> Optional[SkillTemplate]:
|
|
118
|
+
"""按白名单解析单个技能,返回 SkillTemplate 或 None。"""
|
|
119
|
+
try:
|
|
120
|
+
name = (pack_skill.get("name") or "").strip()
|
|
121
|
+
if not name:
|
|
122
|
+
return None
|
|
123
|
+
slots = []
|
|
124
|
+
for sl in (pack_skill.get("slots") or [])[:20]:
|
|
125
|
+
if not isinstance(sl, dict):
|
|
126
|
+
continue
|
|
127
|
+
slots.append(SkillSlot(
|
|
128
|
+
name=(sl.get("name") or "").strip(),
|
|
129
|
+
examples=list(sl.get("examples", []) or [])[:5],
|
|
130
|
+
required=bool(sl.get("required", False)),
|
|
131
|
+
default=sl.get("default"),
|
|
132
|
+
))
|
|
133
|
+
return SkillTemplate(
|
|
134
|
+
name=name,
|
|
135
|
+
description=(pack_skill.get("description") or "")[:1000],
|
|
136
|
+
triggers=list(pack_skill.get("triggers", []) or [])[:20],
|
|
137
|
+
slots=slots,
|
|
138
|
+
steps=list(pack_skill.get("steps", []) or [])[:30],
|
|
139
|
+
examples=list(pack_skill.get("examples", []) or [])[:5],
|
|
140
|
+
tags=list(pack_skill.get("tags", []) or [])[:20],
|
|
141
|
+
confidence=max(0.0, min(1.0, float(pack_skill.get("confidence", 0.0) or 0.0))),
|
|
142
|
+
cluster_size=int(pack_skill.get("cluster_size", 0) or 0),
|
|
143
|
+
)
|
|
144
|
+
except Exception:
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def import_skill_pack(skill_store, pack: Any,
|
|
149
|
+
mode: str = "merge",
|
|
150
|
+
actor: str = "") -> Dict[str, Any]:
|
|
151
|
+
"""导入技能包。mode: merge(UPSERT 合并)/ replace(先删后写)。"""
|
|
152
|
+
v = validate_skill_pack(pack)
|
|
153
|
+
if not v["valid"]:
|
|
154
|
+
return {"success": False, "errors": v["errors"]}
|
|
155
|
+
|
|
156
|
+
skills = pack.get("skills") or []
|
|
157
|
+
templates = []
|
|
158
|
+
for ps in skills:
|
|
159
|
+
t = _parse_skill(ps)
|
|
160
|
+
if t is not None:
|
|
161
|
+
templates.append(t)
|
|
162
|
+
|
|
163
|
+
if mode == "replace":
|
|
164
|
+
# 先删除现有全部技能(仅在包非空时)
|
|
165
|
+
try:
|
|
166
|
+
existing = skill_store.list_skills(limit=MAX_SKILLS_PER_PACK)
|
|
167
|
+
for e in existing:
|
|
168
|
+
skill_store.delete_skill(e.get("name", ""))
|
|
169
|
+
except Exception as ex:
|
|
170
|
+
return {"success": False, "error": f"替换模式清空失败: {ex}"}
|
|
171
|
+
|
|
172
|
+
saved = 0
|
|
173
|
+
if templates:
|
|
174
|
+
try:
|
|
175
|
+
saved = skill_store.save_skills(templates)
|
|
176
|
+
except Exception as ex:
|
|
177
|
+
return {"success": False, "error": f"技能保存失败: {ex}"}
|
|
178
|
+
|
|
179
|
+
imported_cases = 0
|
|
180
|
+
cases = pack.get("cases") or []
|
|
181
|
+
for c in cases[:MAX_CASES_PER_PACK]:
|
|
182
|
+
if not isinstance(c, dict):
|
|
183
|
+
continue
|
|
184
|
+
try:
|
|
185
|
+
skill_store.record_case(
|
|
186
|
+
task=(c.get("task") or "")[:500] or "imported",
|
|
187
|
+
content=(c.get("content") or "")[:2000],
|
|
188
|
+
result=(c.get("result") or "")[:2000],
|
|
189
|
+
outcome=c.get("outcome", "success"),
|
|
190
|
+
duration_seconds=float(c.get("duration_seconds", 0.0) or 0.0),
|
|
191
|
+
tags=list(c.get("tags", []) or [])[:20],
|
|
192
|
+
agent_id=actor or "",
|
|
193
|
+
)
|
|
194
|
+
imported_cases += 1
|
|
195
|
+
except Exception:
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
"success": True,
|
|
200
|
+
"mode": mode,
|
|
201
|
+
"skills_imported": saved,
|
|
202
|
+
"cases_imported": imported_cases,
|
|
203
|
+
"skills_total": len(templates),
|
|
204
|
+
"cases_total": len(cases),
|
|
205
|
+
}
|