wb-agent-runtime-mcp 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.
Files changed (44) hide show
  1. launcher.py +13 -0
  2. scripts/__init__.py +2 -0
  3. scripts/archive.py +306 -0
  4. scripts/backup.py +185 -0
  5. scripts/cli.py +613 -0
  6. scripts/config.py +81 -0
  7. scripts/conflict_resolver.py +517 -0
  8. scripts/deposit.py +294 -0
  9. scripts/embedder.py +391 -0
  10. scripts/embeddings.py +145 -0
  11. scripts/export.py +300 -0
  12. scripts/graph.py +179 -0
  13. scripts/hardware.py +185 -0
  14. scripts/health.py +196 -0
  15. scripts/legacy.py +295 -0
  16. scripts/mcp_server.py +313 -0
  17. scripts/multimodal.py +331 -0
  18. scripts/narrative.py +332 -0
  19. scripts/orchestrator_server.py +250 -0
  20. scripts/rebuild_index.py +197 -0
  21. scripts/retrieval.py +336 -0
  22. scripts/setup.py +125 -0
  23. scripts/store.py +445 -0
  24. scripts/update_check.py +124 -0
  25. scripts/version.py +6 -0
  26. scripts/web_server.py +443 -0
  27. src/scripts/condition_evaluator.py +403 -0
  28. src/scripts/dag_validator.py +532 -0
  29. src/scripts/error_recovery.py +318 -0
  30. src/scripts/flow_controller.py +669 -0
  31. src/scripts/hardware_detector.py +183 -0
  32. src/scripts/orchestrator.py +663 -0
  33. src/scripts/pipeline_registry.py +269 -0
  34. src/scripts/pipeline_reporter.py +467 -0
  35. src/scripts/snapshot_store.py +502 -0
  36. src/scripts/state_recovery.py +239 -0
  37. src/scripts/state_store.py +874 -0
  38. src/scripts/template_lib.py +305 -0
  39. src/scripts/update_checker.py +145 -0
  40. wb_agent_runtime_mcp-1.0.0.dist-info/METADATA +38 -0
  41. wb_agent_runtime_mcp-1.0.0.dist-info/RECORD +44 -0
  42. wb_agent_runtime_mcp-1.0.0.dist-info/WHEEL +5 -0
  43. wb_agent_runtime_mcp-1.0.0.dist-info/entry_points.txt +2 -0
  44. wb_agent_runtime_mcp-1.0.0.dist-info/top_level.txt +4 -0
launcher.py ADDED
@@ -0,0 +1,13 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Universal launcher: run the original mcp_server.py exactly as `python mcp_server.py`."""
3
+ import os
4
+ import runpy
5
+
6
+
7
+ def main():
8
+ here = os.path.dirname(os.path.abspath(__file__))
9
+ runpy.run_path(os.path.join(here, "mcp_server.py"), run_name="__main__")
10
+
11
+
12
+ if __name__ == "__main__":
13
+ main()
scripts/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ # -*- coding: utf-8 -*-
2
+ """长期记忆 / 知识图谱 会思考的进化 AI —— 记忆底座核心包(纯标准库)。"""
scripts/archive.py ADDED
@@ -0,0 +1,306 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 自动归档策略 —— 冷热分层 + 时间衰减,自动控制记忆膨胀。
4
+
5
+ 核心算法:
6
+ 记忆热度分 = 0.6 * ln(access_count + 1) + 0.4 * exp(-days_since_access / 30)
7
+
8
+ 热 (score ≥ 0.7): 保持原样,不处理
9
+ 温 (0.3 ≤ score < 0.7): 抽取式摘要(保留关键句),存入 archive 表
10
+ 冷 (score < 0.3): 仅保留事实抽取结果,高压缩比归档
11
+
12
+ 自动触发: autopilot 末尾自动调用 archive.run()
13
+ 硬件感知: 批处理大小按 hardware.tier 分配
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import math
20
+ import os
21
+ import re
22
+ import sqlite3
23
+ from collections import Counter
24
+ from datetime import datetime, timedelta
25
+
26
+ from . import config, embeddings, store
27
+ from .hardware import get_plan
28
+
29
+
30
+ # ── 热度分层阈值 ─────────────────────────────────────────────────────────
31
+ HOT_THRESHOLD = 0.7 # ≥ 此值为热
32
+ WARM_THRESHOLD = 0.3 # ≥ 此值为温,< 此值为冷
33
+
34
+ # 时间衰减半衰期(天)
35
+ DECAY_HALF_LIFE = 30
36
+
37
+
38
+ def compute_heat(access_count: int, days_since_access: float) -> float:
39
+ """
40
+ 计算记忆热度分(0~1)。
41
+
42
+ 公式:0.6 * ln(access_count + 1) + 0.4 * exp(-days / 30)
43
+ - access_count 越大,热度越高(对数增长,避免刷屏)
44
+ - days_since_access 越大,热度越低(指数衰减)
45
+ """
46
+ freq_score = math.log(access_count + 1) # ln(1)=0, ln(2)=0.69, ln(5)=1.61
47
+ # 归一化到 0~1(ln(10)≈2.3 作为上限)
48
+ freq_normalized = min(freq_score / 2.3, 1.0)
49
+ decay_score = math.exp(-days_since_access / DECAY_HALF_LIFE)
50
+ return 0.6 * freq_normalized + 0.4 * decay_score
51
+
52
+
53
+ def classify_tier(heat: float) -> str:
54
+ """根据热度分返回 hot / warm / cold。"""
55
+ if heat >= HOT_THRESHOLD:
56
+ return "hot"
57
+ if heat >= WARM_THRESHOLD:
58
+ return "warm"
59
+ return "cold"
60
+
61
+
62
+ # ── 归档器 ─────────────────────────────────────────────────────────────────
63
+ class Archiver:
64
+ """自动归档器。"""
65
+
66
+ def __init__(self):
67
+ self._ensure_archive_table()
68
+
69
+ def _ensure_archive_table(self) -> None:
70
+ """确保归档表存在。"""
71
+ conn = store.get_conn()
72
+ conn.execute("""
73
+ CREATE TABLE IF NOT EXISTS archive (
74
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
75
+ memory_id INTEGER NOT NULL,
76
+ original_text TEXT, -- 原始文本(冷归档时保留备份)
77
+ compressed_text TEXT NOT NULL, -- 压缩后的文本
78
+ tier TEXT NOT NULL, -- hot / warm / cold
79
+ heat_score REAL NOT NULL,
80
+ archived_at TEXT NOT NULL,
81
+ FOREIGN KEY(memory_id) REFERENCES memories(id) ON DELETE CASCADE
82
+ )
83
+ """)
84
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_archive_memory ON archive(memory_id)")
85
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_archive_tier ON archive(tier)")
86
+ conn.commit()
87
+
88
+ def analyze(self) -> dict:
89
+ """
90
+ 分析所有记忆的热度分布,返回统计信息。
91
+ 不修改任何数据。
92
+ """
93
+ conn = store.get_conn()
94
+ rows = conn.execute(
95
+ "SELECT id, raw_text, access_count, last_accessed FROM memories"
96
+ ).fetchall()
97
+
98
+ now = datetime.now()
99
+ tiers = {"hot": 0, "warm": 0, "cold": 0}
100
+ heat_scores = []
101
+
102
+ for r in rows:
103
+ last_access = datetime.fromisoformat(r["last_accessed"]) if r["last_accessed"] else now
104
+ days_since = (now - last_access).days + (now - last_access).seconds / 86400.0
105
+ heat = compute_heat(r["access_count"] or 0, days_since)
106
+ tier = classify_tier(heat)
107
+ tiers[tier] += 1
108
+ heat_scores.append(heat)
109
+
110
+ return {
111
+ "total": len(rows),
112
+ "tiers": tiers,
113
+ "avg_heat": round(sum(heat_scores) / max(len(heat_scores), 1), 4),
114
+ "min_heat": round(min(heat_scores) if heat_scores else 0, 4),
115
+ "max_heat": round(max(heat_scores) if heat_scores else 0, 4),
116
+ }
117
+
118
+ def run(self, dry_run: bool = True) -> dict:
119
+ """
120
+ 执行归档。
121
+
122
+ dry_run=True: 只返回计划,不修改数据
123
+ dry_run=False: 真正执行归档
124
+
125
+ 策略:
126
+ - 热记忆:跳过
127
+ - 温记忆:抽取式摘要(保留关键句),存入 archive 表
128
+ - 冷记忆:高压缩比归档(仅保留事实抽取结果),存入 archive 表
129
+ """
130
+ plan = get_plan()
131
+ batch_size = plan.get("batch_size", 60)
132
+
133
+ conn = store.get_conn()
134
+ rows = conn.execute(
135
+ "SELECT id, raw_text, access_count, last_accessed, tokens_json FROM memories"
136
+ ).fetchall()
137
+
138
+ now = datetime.now()
139
+ archived = {"hot": 0, "warm": 0, "cold": 0}
140
+ archived_details = []
141
+
142
+ for r in rows:
143
+ last_access = datetime.fromisoformat(r["last_accessed"]) if r["last_accessed"] else now
144
+ days_since = (now - last_access).days + (now - last_access).seconds / 86400.0
145
+ heat = compute_heat(r["access_count"] or 0, days_since)
146
+ tier = classify_tier(heat)
147
+
148
+ if tier == "hot":
149
+ archived["hot"] += 1
150
+ continue
151
+
152
+ # 检查是否已归档
153
+ existing = conn.execute(
154
+ "SELECT id FROM archive WHERE memory_id=?", (r["id"],)
155
+ ).fetchone()
156
+ if existing:
157
+ archived[tier] += 1
158
+ continue
159
+
160
+ # 按热度分层压缩
161
+ if tier == "warm":
162
+ compressed = self._warm_compress(r["raw_text"] or "")
163
+ else: # cold
164
+ compressed = self._cold_compress(r["raw_text"] or "")
165
+
166
+ if not dry_run:
167
+ conn.execute(
168
+ "INSERT INTO archive(memory_id, original_text, compressed_text, tier, heat_score, archived_at) "
169
+ "VALUES(?,?,?,?,?,?)",
170
+ (r["id"], r["raw_text"] if tier == "cold" else None,
171
+ compressed, tier, heat,
172
+ now.isoformat(timespec="seconds")),
173
+ )
174
+
175
+ archived[tier] += 1
176
+ archived_details.append({
177
+ "memory_id": r["id"],
178
+ "tier": tier,
179
+ "heat": round(heat, 4),
180
+ "compressed_len": len(compressed),
181
+ "original_len": len(r["raw_text"] or ""),
182
+ })
183
+
184
+ if not dry_run:
185
+ conn.commit()
186
+
187
+ return {
188
+ "dry_run": dry_run,
189
+ "total": len(rows),
190
+ "archived": archived,
191
+ "details": archived_details[:20], # 只返回前 20 条详情
192
+ "batch_size": batch_size,
193
+ "tier": plan["tier"],
194
+ }
195
+
196
+ def _warm_compress(self, text: str) -> str:
197
+ """
198
+ 温记忆压缩:抽取式摘要,保留关键句。
199
+ 压缩比约 30~50%。
200
+ """
201
+ sents = []
202
+ for s in re.split(r"[。!?\n;;]", text):
203
+ s = s.strip()
204
+ if len(s) >= 6:
205
+ sents.append(s)
206
+ if not sents:
207
+ return text[:100] if text else ""
208
+
209
+ # 按 token 频次打分
210
+ freq = Counter()
211
+ for s in sents:
212
+ for tk in embeddings.tokenize(s):
213
+ freq[tk] += 1
214
+
215
+ scored = sorted(sents, key=lambda s: sum(freq.get(tk, 0) for tk in embeddings.tokenize(s)),
216
+ reverse=True)
217
+ # 保留 top-3 关键句
218
+ return ";".join(scored[:3])
219
+
220
+ def _cold_compress(self, text: str) -> str:
221
+ """
222
+ 冷记忆压缩:仅保留事实抽取结果(实体+谓词+值)。
223
+ 压缩比约 70~90%。
224
+ """
225
+ # 极简事实抽取:找「X的Y是Z」模式
226
+ facts = []
227
+ for m in re.finditer(r"([\u4e00-\u9fff]{1,10})的([\u4e00-\u9fff]{1,8})[是为::]\s*([^\n。;;]{1,30})", text):
228
+ entity, predicate, value = m.groups()
229
+ # 清理首尾噪声
230
+ entity = entity.strip()
231
+ predicate = predicate.strip()
232
+ value = value.strip()
233
+ if entity and predicate and value:
234
+ facts.append("%s的%s是%s" % (entity, predicate, value))
235
+
236
+ if facts:
237
+ return " | ".join(facts[:5]) # 最多保留 5 条事实
238
+
239
+ # 回退:截断到 50 字
240
+ return text[:50] + "..." if len(text) > 50 else text
241
+
242
+ def restore(self, memory_id: int) -> str | None:
243
+ """
244
+ 从归档恢复原始文本(仅冷归档有原始文本备份)。
245
+ 返回原始文本,若不存在返回 None。
246
+ """
247
+ conn = store.get_conn()
248
+ row = conn.execute(
249
+ "SELECT original_text, tier FROM archive WHERE memory_id=?", (memory_id,)
250
+ ).fetchone()
251
+ if not row:
252
+ return None
253
+ return row["original_text"] # 冷归档有,温归档为 None
254
+
255
+ def get_archive_stats(self) -> dict:
256
+ """获取归档统计信息。"""
257
+ conn = store.get_conn()
258
+ total = conn.execute("SELECT COUNT(*) FROM archive").fetchone()[0]
259
+ by_tier = {}
260
+ for tier in ("hot", "warm", "cold"):
261
+ cnt = conn.execute("SELECT COUNT(*) FROM archive WHERE tier=?", (tier,)).fetchone()[0]
262
+ by_tier[tier] = cnt
263
+
264
+ # 计算压缩节省的空间
265
+ orig_total = conn.execute(
266
+ "SELECT SUM(LENGTH(original_text)) FROM archive"
267
+ ).fetchone()[0] or 0
268
+ comp_total = conn.execute(
269
+ "SELECT SUM(LENGTH(compressed_text)) FROM archive"
270
+ ).fetchone()[0] or 0
271
+
272
+ saving_pct = (1 - comp_total / max(orig_total, 1)) * 100 if orig_total > 0 else 0
273
+
274
+ return {
275
+ "total_archived": total,
276
+ "by_tier": by_tier,
277
+ "original_size_kb": round(orig_total / 1024, 2),
278
+ "compressed_size_kb": round(comp_total / 1024, 2),
279
+ "saving_percent": round(saving_pct, 1),
280
+ }
281
+
282
+
283
+ # ── 便捷函数 ─────────────────────────────────────────────────────────────────
284
+ def analyze() -> dict:
285
+ """分析记忆热度分布。"""
286
+ return Archiver().analyze()
287
+
288
+
289
+ def run_archive(dry_run: bool = True) -> dict:
290
+ """执行归档。"""
291
+ return Archiver().run(dry_run=dry_run)
292
+
293
+
294
+ def get_archive_stats() -> dict:
295
+ """获取归档统计。"""
296
+ return Archiver().get_archive_stats()
297
+
298
+
299
+ if __name__ == "__main__":
300
+ import pprint
301
+ print("=== 热度分析 ===")
302
+ pprint.pprint(analyze())
303
+ print("\n=== 归档计划 ===")
304
+ pprint.pprint(run_archive(dry_run=True))
305
+ print("\n=== 归档统计 ===")
306
+ pprint.pprint(get_archive_stats())
scripts/backup.py ADDED
@@ -0,0 +1,185 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 备份 / 恢复 —— 导出、备份、恢复。
4
+
5
+ 默认目标:本地文件(零密钥、零联网、立即可用)。
6
+ 可插拔目标:百度网盘(baidunetdisk)。该适配器仅在「用户已配置本地 token」时启用;
7
+ 未配置则给出明确指引,绝不偷偷联网或要求密钥。
8
+
9
+ 设计原则:
10
+ - 备份是「可恢复」的:snapshot(JSON) + 每日日志原文一并打包到备份目录。
11
+ - 保留最近 N 份(config.BACKUP_KEEP),自动清理过旧备份。
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import os
18
+ import shutil
19
+ from datetime import datetime
20
+
21
+ from . import config, health, store, embeddings
22
+
23
+
24
+ def export_local(backup_dir: str | None = None) -> dict:
25
+ """
26
+ 导出到本地备份目录:快照 + 每日日志原文。
27
+ 返回 {backup_path, snapshot, logs_copied}。
28
+ """
29
+ config.ensure_dirs()
30
+ if backup_dir is None:
31
+ backup_dir = str(config.ZWJH_DIR / "backups")
32
+ stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
33
+ dest = os.path.join(backup_dir, f"zwjh_backup_{stamp}")
34
+ os.makedirs(dest, exist_ok=True)
35
+
36
+ # 1) 快照(图谱 + 事实 + 配置)
37
+ snap = health.snapshot(os.path.join(dest, "snapshot.json"))
38
+
39
+ # 2) 每日日志原文
40
+ logs_copied = 0
41
+ logs_src = config.MEMORY_DIR
42
+ logs_dst = os.path.join(dest, "daily_logs")
43
+ os.makedirs(logs_dst, exist_ok=True)
44
+ for fp in sorted(logs_src.glob("20??-??-??.md")):
45
+ try:
46
+ shutil.copy2(fp, os.path.join(logs_dst, fp.name))
47
+ logs_copied += 1
48
+ except Exception:
49
+ pass
50
+
51
+ # 3) 清理旧备份(保留最近 BACKUP_KEEP 份)
52
+ _prune_backups(backup_dir)
53
+
54
+ return {"backup_path": dest, "snapshot": snap, "logs_copied": logs_copied}
55
+
56
+
57
+ def _prune_backups(backup_dir: str) -> None:
58
+ try:
59
+ items = sorted(
60
+ [d for d in os.listdir(backup_dir) if d.startswith("zwjh_backup_")],
61
+ reverse=True,
62
+ )
63
+ for old in items[config.BACKUP_KEEP:]:
64
+ shutil.rmtree(os.path.join(backup_dir, old), ignore_errors=True)
65
+ except Exception:
66
+ pass
67
+
68
+
69
+ def restore_local(backup_path: str, include_logs: bool = True) -> dict:
70
+ """从本地备份恢复(快照写回库;可选覆盖每日日志)。"""
71
+ snap_file = os.path.join(backup_path, "snapshot.json")
72
+ if not os.path.exists(snap_file):
73
+ return {"status": "error", "reason": "no_snapshot"}
74
+ with open(snap_file, "r", encoding="utf-8") as f:
75
+ data = json.load(f)
76
+
77
+ # 写回实体/关系/事实
78
+ conn = store.get_conn()
79
+ # 清空派生索引(保留每日日志原文)
80
+ store.wipe_memories_only()
81
+ ent_id_map = {}
82
+ for e in data.get("entities", []):
83
+ new_id = store.upsert_entity(e["type"], e["name"],
84
+ json.loads(e.get("aliases_json", "[]")),
85
+ e.get("importance", 0.5))
86
+ ent_id_map[e["id"]] = new_id
87
+ for r in data.get("relations", []):
88
+ frm = ent_id_map.get(r["from_id"])
89
+ to = ent_id_map.get(r["to_id"])
90
+ if frm and to:
91
+ store.add_relation(frm, to, r["relation"], r.get("weight", 1.0),
92
+ r.get("confidence", 1.0))
93
+ # 事实写回
94
+ for fct in data.get("facts", []):
95
+ eid = ent_id_map.get(fct["entity_id"])
96
+ if eid:
97
+ conn.execute(
98
+ "INSERT INTO facts(entity_id, predicate, value, valid_from, valid_to, "
99
+ "superseded, source_memory_id, created_at) VALUES(?,?,?,?,?,?,?,?)",
100
+ (eid, fct["predicate"], fct["value"], fct.get("valid_from"),
101
+ fct.get("valid_to"), fct.get("superseded", 0),
102
+ None, fct.get("created_at")),
103
+ )
104
+
105
+ # 记忆写回(快照含记忆,避免恢复后记忆表为空导致数据丢失)
106
+ for m in data.get("memories", []):
107
+ try:
108
+ toks = json.loads(m.get("tokens_json", "[]"))
109
+ except Exception:
110
+ toks = embeddings.tokenize(m.get("raw_text", ""))
111
+ store.add_memory(
112
+ m.get("day", config.today_str()),
113
+ m.get("source", "conversation"),
114
+ m.get("raw_text", ""),
115
+ m.get("norm_hash", ""),
116
+ toks,
117
+ importance=float(m.get("importance", 0.5)),
118
+ )
119
+ conn.commit()
120
+
121
+ logs_restored = 0
122
+ if include_logs:
123
+ logs_src = os.path.join(backup_path, "daily_logs")
124
+ if os.path.isdir(logs_src):
125
+ for fp in os.listdir(logs_src):
126
+ if fp.endswith(".md"):
127
+ try:
128
+ shutil.copy2(os.path.join(logs_src, fp),
129
+ os.path.join(str(config.MEMORY_DIR), fp))
130
+ logs_restored += 1
131
+ except Exception:
132
+ pass
133
+ return {"status": "ok", "entities": len(ent_id_map),
134
+ "logs_restored": logs_restored}
135
+
136
+
137
+ # ── 百度网盘适配器(可插拔,零密钥默认不可用) ──────────────────────────────
138
+ def backup_baidunetdisk(backup_dir: str | None = None) -> dict:
139
+ """
140
+ 备份到百度网盘。
141
+
142
+ 仅在用户本地已配置百度网盘访问凭证时可用(凭证存于本地 config,不进技能包)。
143
+ 未配置时返回明确指引,不报错、不联网试探。
144
+ """
145
+ cfg = config.load_config()
146
+ token = cfg.get("baidunetdisk", {}).get("access_token")
147
+ if not token:
148
+ return {
149
+ "status": "unavailable",
150
+ "reason": "未配置百度网盘凭证",
151
+ "hint": "在 config.json 写入 baidunetdisk.access_token 即可启用;"
152
+ "或先运行「本地备份」再做手动上传。",
153
+ }
154
+ # 已配置:本地先导出,再调用上传适配器(mixin 接口,便于替换实现)
155
+ local = export_local(backup_dir)
156
+ return _upload_to_baidu(local["backup_path"], token)
157
+
158
+
159
+ def _upload_to_baidu(local_path: str, token: str) -> dict:
160
+ """
161
+ 上传适配器占位实现:优先使用已安装的 `bypy` CLI(需用户自行授权)。
162
+ 不内置任何密钥;这里只负责调用已授权的本地客户端。
163
+ """
164
+ import subprocess
165
+
166
+ try:
167
+ proc = subprocess.run(
168
+ ["bypy", "upload", local_path, "/zwjh_backup/"],
169
+ capture_output=True, text=True, timeout=120,
170
+ )
171
+ if proc.returncode == 0:
172
+ return {"status": "ok", "via": "bypy", "path": local_path}
173
+ return {"status": "error", "reason": proc.stderr[:200]}
174
+ except FileNotFoundError:
175
+ return {
176
+ "status": "unavailable",
177
+ "reason": "未找到 bypy 客户端",
178
+ "hint": "pip install bypy 并完成授权后可启用百度网盘自动备份。",
179
+ }
180
+ except Exception as e:
181
+ return {"status": "error", "reason": str(e)[:200]}
182
+
183
+
184
+ if __name__ == "__main__":
185
+ print(export_local())