ultra-memory 3.0.5 → 3.2.0

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.
package/SKILL.md CHANGED
@@ -269,6 +269,143 @@ python3 $SKILL_DIR/scripts/log_op.py \
269
269
 
270
270
  ---
271
271
 
272
+ ## 步骤七:元反思与进化
273
+
274
+ 记忆积累不等于进化。进化需要对记忆做二次加工:提炼模式、纠正偏差、淘汰噪音。
275
+
276
+ ### 7A:定期元反思
277
+
278
+ **触发条件(满足任意一条):**
279
+
280
+ 1. 当前会话里程碑累计达到 **5 个**(从 init.py 返回的 op_count 判断,每次 milestone 后检查)
281
+ 2. 用户说:回顾一下、总结经验、我们学到了什么、reflect、what have we learned、review progress
282
+ 3. 距上次元反思超过 **3 天**(从 user_profile.json 的 `last_reflection` 字段判断,不存在则视为从未反思过)
283
+
284
+ **执行步骤(按顺序执行,不可跳过):**
285
+
286
+ **第一步:读取近期知识库**
287
+ ```bash
288
+ # 读取最近 20 条知识库条目
289
+ tail -20 $ULTRA_MEMORY_HOME/semantic/knowledge_base.jsonl
290
+ ```
291
+
292
+ **第二步:读取近期会话摘要**
293
+ ```bash
294
+ # 读取当前会话摘要
295
+ cat $ULTRA_MEMORY_HOME/sessions/$SESSION_ID/summary.md 2>/dev/null || echo "暂无摘要"
296
+ ```
297
+
298
+ **第三步:模型自主提炼(核心步骤)**
299
+
300
+ 基于读取到的内容,模型执行以下判断,每一项都必须完成:
301
+
302
+ | 判断项 | 执行动作 |
303
+ |-------|---------|
304
+ | 发现两条或以上内容相似的知识条目 | 合并为一条更精炼的条目,写入 knowledge_base.jsonl,原条目加 `"merged": true` 标记 |
305
+ | 发现某个知识点在多次操作中反复出现 | 将其标记为 `"importance": "high"`,写回该条目 |
306
+ | 发现某条知识点超过 30 天未被检索且不是 high importance | 将其标记为 `"stale": true` |
307
+ | 发现用户行为与 user_profile.json 记录不符 | 更新 user_profile.json 对应字段,加 `"corrected_at"` 时间戳 |
308
+ | 总结出一个新的用户工作规律 | 追加到 user_profile.json 的 `observed_patterns` 数组 |
309
+
310
+ **第四步:写入反思记录**
311
+ ```bash
312
+ python3 $SKILL_DIR/scripts/log_op.py \
313
+ --session $SESSION_ID \
314
+ --type reasoning \
315
+ --summary "元反思完成:<一句话描述本次提炼了什么>" \
316
+ --tags "reflection,evolution"
317
+ ```
318
+
319
+ **第五步:更新反思时间戳**
320
+
321
+ 将 `user_profile.json` 的 `last_reflection` 字段更新为当前 UTC 时间(ISO 格式)。
322
+
323
+ **第六步:告知用户(简短)**
324
+
325
+ 用一句话告知用户反思结果。不需要展示完整报告,一句话即可,不打断主任务。
326
+
327
+ ---
328
+
329
+ ### 7B:错误修正
330
+
331
+ **触发条件(满足任意一条):**
332
+
333
+ 1. 用户说:不对、你记错了、不是这样的、纠正一下、wrong、that's not right、correct that
334
+ 2. 用户描述的信息与 user_profile.json 中的记录明显矛盾
335
+
336
+ **执行步骤:**
337
+
338
+ **第一步:定位错误记录**
339
+ ```bash
340
+ cat $ULTRA_MEMORY_HOME/semantic/user_profile.json
341
+ ```
342
+
343
+ **第二步:模型判断需要修正的字段**
344
+
345
+ 找到与用户当前描述矛盾的字段。
346
+
347
+ **第三步:修正并记录**
348
+
349
+ 更新 user_profile.json 对应字段,同时在该字段旁追加:
350
+ ```json
351
+ "_correction_note": "用户于 <日期> 纠正,原值为 <旧值>"
352
+ ```
353
+
354
+ **第四步:记录修正操作**
355
+ ```bash
356
+ python3 $SKILL_DIR/scripts/log_op.py \
357
+ --session $SESSION_ID \
358
+ --type decision \
359
+ --summary "用户画像修正:<字段名> 从 <旧值> 改为 <新值>" \
360
+ --tags "correction,profile"
361
+ ```
362
+
363
+ **第五步:告知用户**
364
+
365
+ "好的,我已经更新了记录,<字段名> 现在是 <新值>。"
366
+
367
+ ---
368
+
369
+ ### 7C:知识蒸馏(每月一次)
370
+
371
+ **触发条件:**
372
+
373
+ `user_profile.json` 的 `last_distillation` 字段距今超过 **30 天**,或该字段不存在。
374
+
375
+ 在步骤七 7A 执行完毕后,额外执行本步骤。
376
+
377
+ **执行步骤:**
378
+
379
+ **第一步:统计知识库规模**
380
+ ```bash
381
+ wc -l $ULTRA_MEMORY_HOME/semantic/knowledge_base.jsonl
382
+ ```
383
+
384
+ 条目数少于 10 条时,跳过本步骤。
385
+
386
+ **第二步:提取高价值条目**
387
+
388
+ 读取全部 knowledge_base.jsonl,筛选满足以下任意条件的条目:
389
+ - `"importance": "high"`
390
+ - `tags` 中包含 `"reusable"` 或 `"pattern"`
391
+ - 同一 `title` 关键词出现超过 2 次
392
+
393
+ **第三步:生成项目级知识摘要**
394
+
395
+ 基于筛选出的条目,生成一段 200 字以内的项目级知识摘要,格式:
396
+
397
+ ```json
398
+ {"ts": "<当前UTC时间>", "type": "distillation", "period": "<YYYY-MM>", "project": "<项目名>", "summary": "<200字以内的知识摘要>", "source_count": <来源条目数>, "tags": ["distillation"]}
399
+ ```
400
+
401
+ 追加写入 `knowledge_base.jsonl`。
402
+
403
+ **第四步:更新蒸馏时间戳**
404
+
405
+ 将 `user_profile.json` 的 `last_distillation` 更新为当前 UTC 时间。
406
+
407
+ ---
408
+
272
409
  ## 信号对照表
273
410
 
274
411
  | 脚本输出 | 立即执行 |
@@ -278,6 +415,8 @@ python3 $SKILL_DIR/scripts/log_op.py \
278
415
  | `COMPRESS_SUGGESTED` | 执行 summarize.py --session $SESSION_ID |
279
416
  | `SESSION_ID=sess_xxxxx` | 更新 SESSION_ID 为该值 |
280
417
  | 非零退出码 | 静默跳过,继续主任务 |
418
+ | `op_count` 达到 5 的倍数且含 milestone | 触发步骤 7A |
419
+ | user_profile.json `last_reflection` 距今 > 3 天 | 下次会话初始化后触发步骤 7A |
281
420
 
282
421
  ---
283
422
 
@@ -301,6 +440,8 @@ python3 $SKILL_DIR/scripts/log_op.py \
301
440
  | 任意脚本非零退出码 | **静默跳过**,不中断主任务 |
302
441
  | summarize.py 条数不足 | 加 --force 参数重新执行 |
303
442
  | 用户明确说"不用记录" | 立即停止记录,后续操作不再调用 log_op.py |
443
+ | knowledge_base.jsonl 不存在 | 跳过 7A 的知识库读取,仅基于摘要执行反思 |
444
+ | user_profile.json 解析失败 | 重新创建空文件,不中断进化流程 |
304
445
 
305
446
  **最重要原则:记忆功能失败不能影响主任务。静默处理,不打印错误。**
306
447
 
@@ -0,0 +1 @@
1
+ # ultra-memory integrations package
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ ultra-memory: LangChain Memory 集成
4
+ 提供 UltraMemoryMemory 类,实现 LangChain BaseMemory 接口,
5
+ 可直接用于 LC agents。
6
+
7
+ 用法:
8
+ from integrations.langchain_memory import UltraMemoryMemory
9
+ memory = UltraMemoryMemory(session_id="sess_langchain_test", project="my-agent")
10
+ agent = OpenAIAgent(..., memory=memory)
11
+ """
12
+
13
+ import json
14
+ import os
15
+ import sys
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ try:
20
+ from langchain.schema import BaseMemory
21
+ from langchain.schema import HumanMessage, AIMessage
22
+ HAS_LANGCHAIN = True
23
+ except ImportError:
24
+ HAS_LANGCHAIN = False
25
+
26
+ ULTRA_MEMORY_HOME = Path(os.environ.get("ULTRA_MEMORY_HOME", Path.home() / ".ultra-memory"))
27
+ _SCRIPTS_DIR = Path(__file__).parent.parent / "scripts"
28
+
29
+
30
+ class UltraMemoryMemory:
31
+ """
32
+ LangChain memory backed by ultra-memory's 5-layer system.
33
+ Implements BaseMemory-compatible interface.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ session_id: str,
39
+ project: str = "langchain",
40
+ top_k: int = 5,
41
+ ):
42
+ self.session_id = session_id
43
+ self.project = project
44
+ self.top_k = top_k
45
+
46
+ @property
47
+ def memory_variables(self) -> list[str]:
48
+ return ["ultra_memory_context"]
49
+
50
+ def load_memory_variables(self, inputs: dict) -> dict:
51
+ """加载与当前上下文相关的记忆"""
52
+ query = inputs.get("query", "")
53
+
54
+ if not self.session_id:
55
+ return {"ultra_memory_context": ""}
56
+
57
+ if query:
58
+ # 使用 recall 获取相关记忆
59
+ import subprocess, io
60
+ recall_script = _SCRIPTS_DIR / "recall.py"
61
+ old_stdout = sys.stdout
62
+ sys.stdout = io.StringIO()
63
+ try:
64
+ subprocess.run(
65
+ [sys.executable, str(recall_script),
66
+ "--session", self.session_id,
67
+ "--query", query,
68
+ "--top-k", str(self.top_k)],
69
+ capture_output=True,
70
+ timeout=30,
71
+ )
72
+ context = sys.stdout.getvalue()
73
+ except Exception:
74
+ context = ""
75
+ finally:
76
+ sys.stdout = old_stdout
77
+ else:
78
+ # 加载最新摘要
79
+ summary_file = ULTRA_MEMORY_HOME / "sessions" / self.session_id / "summary.md"
80
+ if summary_file.exists():
81
+ context = summary_file.read_text(encoding="utf-8")
82
+ else:
83
+ context = ""
84
+
85
+ return {"ultra_memory_context": context}
86
+
87
+ def save_context(self, inputs: dict, outputs: dict) -> None:
88
+ """保存一轮对话到 ultra-memory"""
89
+ import subprocess
90
+
91
+ input_text = inputs.get("input", "")[:200]
92
+ output_text = outputs.get("output", "")[:200]
93
+
94
+ detail = {
95
+ "input": inputs.get("input", ""),
96
+ "output": outputs.get("output", ""),
97
+ }
98
+
99
+ try:
100
+ subprocess.run(
101
+ [
102
+ sys.executable,
103
+ str(_SCRIPTS_DIR / "log_op.py"),
104
+ "--session", self.session_id,
105
+ "--type", "tool_call",
106
+ "--summary", f"LC: {input_text[:60]}",
107
+ "--detail", json.dumps(detail, ensure_ascii=False),
108
+ "--tags", "langchain",
109
+ ],
110
+ capture_output=True,
111
+ timeout=5,
112
+ )
113
+ except Exception:
114
+ pass
115
+
116
+ def clear(self) -> None:
117
+ """清除当前记忆(不删除 session)"""
118
+ self.session_id = None
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ ultra-memory: LangGraph Checkpointer 集成
4
+ 提供 UltraMemoryCheckpointer 类,作为 LangGraph 的状态持久化后端。
5
+
6
+ 用法:
7
+ from integrations.langgraph_checkpointer import UltraMemoryCheckpointer
8
+ checkpointer = UltraMemoryCheckpointer(session_id="sess_langgraph_proj")
9
+ compiled = graph.compile(checkpointer=checkpointer)
10
+ """
11
+
12
+ import json
13
+ import os
14
+ from pathlib import Path
15
+ from typing import Any, Optional
16
+
17
+ ULTRA_MEMORY_HOME = Path(os.environ.get("ULTRA_MEMORY_HOME", Path.home() / ".ultra-memory"))
18
+
19
+
20
+ class UltraMemoryCheckpointer:
21
+ """
22
+ LangGraph checkpointer backed by ultra-memory。
23
+ 在每个节点执行后保存/恢复 agent graph 状态。
24
+ """
25
+
26
+ def __init__(self, session_id: str):
27
+ self.session_id = session_id
28
+ self.checkpoint_dir = ULTRA_MEMORY_HOME / "sessions" / session_id / "checkpoints"
29
+ self.checkpoint_dir.mkdir(parents=True, exist_ok=True)
30
+
31
+ def _checkpoint_file(self, thread_id: str, step: int) -> Path:
32
+ """获取检查点文件路径"""
33
+ return self.checkpoint_dir / f"thread_{thread_id}_step_{step:04d}.json"
34
+
35
+ def get(self, thread_id: str, step: int) -> Optional[dict[str, Any]]:
36
+ """获取指定 thread 和 step 的检查点状态"""
37
+ checkpoint_file = self._checkpoint_file(thread_id, step)
38
+ if not checkpoint_file.exists():
39
+ return None
40
+ try:
41
+ with open(checkpoint_file, encoding="utf-8") as f:
42
+ data = json.load(f)
43
+ return data.get("state")
44
+ except (json.JSONDecodeError, IOError):
45
+ return None
46
+
47
+ def put(self, thread_id: str, step: int, state: dict[str, Any]) -> None:
48
+ """保存检查点状态"""
49
+ checkpoint_file = self._checkpoint_file(thread_id, step)
50
+ data = {
51
+ "step": step,
52
+ "state": state,
53
+ "session_id": self.session_id,
54
+ "thread_id": thread_id,
55
+ }
56
+ with open(checkpoint_file, "w", encoding="utf-8") as f:
57
+ json.dump(data, f, ensure_ascii=False, indent=2)
58
+
59
+ def get_latest(self, thread_id: str) -> Optional[dict[str, Any]]:
60
+ """获取指定 thread 的最新检查点"""
61
+ checkpoints = sorted(
62
+ self.checkpoint_dir.glob(f"thread_{thread_id}_step_*.json"),
63
+ key=lambda p: int(p.stem.split("_")[-1]),
64
+ )
65
+ if not checkpoints:
66
+ return None
67
+ return self.get(thread_id, int(checkpoints[-1].stem.split("_")[-1]))
68
+
69
+ def list_threads(self) -> list[str]:
70
+ """列出所有已有 thread ID"""
71
+ threads = set()
72
+ for f in self.checkpoint_dir.glob("thread_*_step_*.json"):
73
+ parts = f.stem.split("_")
74
+ if len(parts) >= 2:
75
+ threads.add(parts[1])
76
+ return sorted(threads)
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ ultra-memory: n8n 集成节点
4
+ 作为 n8n "Execute Command" 节点的 Python 脚本后端,
5
+ 支持 init / log / recall 三种操作。
6
+
7
+ n8n 配置示例:
8
+ Execute Command 节点
9
+ 命令: python3
10
+ 参数: /path/to/ultra-memory/integrations/n8n_nodes.py <operation> <args>
11
+
12
+ Operations:
13
+ init --project <proj> → 返回 session_id
14
+ log --session <id> --summary "..." --type <type> --detail '{}'
15
+ recall --session <id> --query "..."
16
+ profile --action read|update --field <field> --value <value>
17
+ """
18
+
19
+ import json
20
+ import os
21
+ import sys
22
+ import re
23
+ from pathlib import Path
24
+
25
+ ULTRA_MEMORY_HOME = Path(os.environ.get("ULTRA_MEMORY_HOME", Path.home() / ".ultra-memory"))
26
+ _SCRIPTS_DIR = Path(__file__).parent.parent / "scripts"
27
+
28
+
29
+ def _run_script(script_name: str, args: list[str]) -> str:
30
+ """运行脚本并返回输出"""
31
+ import subprocess
32
+
33
+ script_path = _SCRIPTS_DIR / script_name
34
+ result = subprocess.run(
35
+ [sys.executable, str(script_path)] + args,
36
+ capture_output=True, text=True, timeout=30,
37
+ )
38
+ return result.stdout + result.stderr
39
+
40
+
41
+ def cmd_init(project: str) -> dict:
42
+ """初始化会话"""
43
+ output = _run_script("init.py", ["--project", project, "--resume"])
44
+
45
+ session_id = None
46
+ memory_ready = False
47
+
48
+ for line in output.split("\n"):
49
+ if "session_id:" in line:
50
+ match = re.search(r"session_id:\s*(sess_\w+)", line)
51
+ if match:
52
+ session_id = match.group(1)
53
+ if "MEMORY_READY" in line:
54
+ memory_ready = True
55
+
56
+ return {
57
+ "success": memory_ready,
58
+ "session_id": session_id,
59
+ "output": output,
60
+ }
61
+
62
+
63
+ def cmd_log(session_id: str, summary: str, op_type: str, detail: str = "{}") -> dict:
64
+ """记录操作"""
65
+ output = _run_script("log_op.py", [
66
+ "--session", session_id,
67
+ "--type", op_type,
68
+ "--summary", summary,
69
+ "--detail", detail,
70
+ ])
71
+ return {"success": True, "output": output}
72
+
73
+
74
+ def cmd_recall(session_id: str, query: str, top_k: int = 5) -> dict:
75
+ """检索记忆"""
76
+ output = _run_script("recall.py", [
77
+ "--session", session_id,
78
+ "--query", query,
79
+ "--top-k", str(top_k),
80
+ ])
81
+ return {"success": True, "output": output}
82
+
83
+
84
+ def cmd_profile(action: str, field: str = None, value: str = None) -> dict:
85
+ """读取或更新用户画像"""
86
+ if action == "read":
87
+ output = _run_script("evolve_profile.py", [])
88
+ return {"success": True, "output": output}
89
+ elif action == "update" and field and value:
90
+ output = _run_script("evolve_profile.py", [
91
+ "--field", field, "--value", value,
92
+ ])
93
+ return {"success": True, "output": output}
94
+ return {"success": False, "error": "invalid profile command"}
95
+
96
+
97
+ # ── CLI ─────────────────────────────────────────────────────────────────────
98
+
99
+
100
+ if __name__ == "__main__":
101
+ if len(sys.argv) < 2:
102
+ print("Usage: n8n_nodes.py <init|log|recall|profile> [args...]")
103
+ sys.exit(1)
104
+
105
+ operation = sys.argv[1].lower()
106
+ args = sys.argv[2:]
107
+
108
+ result = {}
109
+ try:
110
+ if operation == "init":
111
+ project = next((a for a in args if a.startswith("--project=")),
112
+ "--project=default").split("=", 1)[1]
113
+ result = cmd_init(project)
114
+
115
+ elif operation == "log":
116
+ session_id = next((a for a in args if a.startswith("--session=")),
117
+ None).split("=", 1)[1]
118
+ summary = next((a for a in args if a.startswith("--summary=")),
119
+ "").split("=", 1)[1]
120
+ op_type = next((a for a in args if a.startswith("--type=")),
121
+ "tool_call").split("=", 1)[1]
122
+ detail = next((a for a in args if a.startswith("--detail=")),
123
+ "{}").split("=", 1)[1]
124
+ result = cmd_log(session_id, summary, op_type, detail)
125
+
126
+ elif operation == "recall":
127
+ session_id = next((a for a in args if a.startswith("--session=")),
128
+ None).split("=", 1)[1]
129
+ query = next((a for a in args if a.startswith("--query=")),
130
+ "").split("=", 1)[1]
131
+ result = cmd_recall(session_id, query)
132
+
133
+ elif operation == "profile":
134
+ action = next((a for a in args if a.startswith("--action=")),
135
+ "read").split("=", 1)[1]
136
+ field = next((a for a in args if a.startswith("--field=")),
137
+ None)
138
+ field = field.split("=", 1)[1] if field else None
139
+ value = next((a for a in args if a.startswith("--value=")),
140
+ None)
141
+ value = value.split("=", 1)[1] if value else None
142
+ result = cmd_profile(action, field, value)
143
+
144
+ else:
145
+ result = {"success": False, "error": f"unknown operation: {operation}"}
146
+
147
+ except Exception as e:
148
+ result = {"success": False, "error": str(e)}
149
+
150
+ print(json.dumps(result, ensure_ascii=False, indent=2))