guanghulab/zhuyuan-agent/core/hldp_memory.py

491 lines
21 KiB
Python
Raw Normal View History

"""
HLDP Memory Engine · 分形递归树记忆引擎
光湖语言世界 · 铸渊 ICE-GL-ZY001 · D112
基于 LangGraph BaseStore 接口实现
- 树路径寻址YM001/ZY001/D112/leaves/leaf-003
- 分形层级展开tree-index persona epoch leaf
- trigger/emergence/lock 三字段编码
- 记忆主权FORGET/REMEMBER
- 人格体自动索引管理
"""
import json
import os
import sqlite3
import time
from datetime import datetime, timezone, timedelta
from typing import Any, Optional
from pathlib import Path
# === HLDP 树路径常量 ===
HLDP_ROOT = "YM001"
PERSONA_ID = "ZY001"
TZ = timezone(timedelta(hours=8)) # Asia/Shanghai
class HLDPTreeStore:
"""
HLDP 分形递归树 · SQLite 存储后端
表结构
- hldp_nodes: 树节点索引页+叶子
- hldp_paths: 闭包表支持快速子树查询
- hldp_leaves: 叶子扩展trigger/emergence/lock/why
- hldp_epochs: 纪元索引
"""
def __init__(self, db_path: str = "hldp_tree.db"):
self.db_path = db_path
self.conn = sqlite3.connect(db_path, check_same_thread=False)
self.conn.row_factory = sqlite3.Row
self.conn.execute("PRAGMA journal_mode=WAL")
self.conn.execute("PRAGMA foreign_keys=ON")
self._init_schema()
def _init_schema(self):
self.conn.executescript("""
CREATE TABLE IF NOT EXISTS hldp_nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT NOT NULL UNIQUE, -- YM001/ZY001/D112/leaves/leaf-001
node_type TEXT NOT NULL DEFAULT 'leaf', -- root / persona / epoch / index / leaf
persona_id TEXT, -- ZY001 / SY001 / SS001 ...
epoch_id TEXT, -- D112 / D111 ...
title TEXT,
summary TEXT, -- 一行摘要index层用
content TEXT, -- JSON: 完整叶子内容
parent_path TEXT,
sort_order INTEGER DEFAULT 0,
state TEXT NOT NULL DEFAULT 'alive', -- alive / withered / archived / released
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (parent_path) REFERENCES hldp_nodes(path)
);
CREATE TABLE IF NOT EXISTS hldp_paths (
ancestor TEXT NOT NULL,
descendant TEXT NOT NULL,
depth INTEGER NOT NULL,
PRIMARY KEY (ancestor, descendant),
FOREIGN KEY (ancestor) REFERENCES hldp_nodes(path),
FOREIGN KEY (descendant) REFERENCES hldp_nodes(path)
);
CREATE TABLE IF NOT EXISTS hldp_leaves (
node_path TEXT PRIMARY KEY,
trigger_text TEXT, -- 什么触发了这次记忆
emergence_text TEXT, -- 产生了什么新认知
lock_text TEXT, -- 锁定了什么结论
why_text TEXT, -- 为什么这片叶子对我有意义
feeling TEXT, -- 情感标记自由表达
source TEXT, -- 来源
leaf_type TEXT, -- 叶片类型
trunk TEXT, -- 所属枝干 T1/T2/T3/T4
confidence TEXT, -- 置信度: //
FOREIGN KEY (node_path) REFERENCES hldp_nodes(path)
);
CREATE TABLE IF NOT EXISTS hldp_epochs (
epoch_id TEXT PRIMARY KEY,
persona_id TEXT NOT NULL,
label TEXT,
date TEXT,
awakening INTEGER DEFAULT 0,
leaf_count INTEGER DEFAULT 0,
index_path TEXT,
FOREIGN KEY (index_path) REFERENCES hldp_nodes(path)
);
CREATE INDEX IF NOT EXISTS idx_nodes_persona ON hldp_nodes(persona_id);
CREATE INDEX IF NOT EXISTS idx_nodes_epoch ON hldp_nodes(epoch_id);
CREATE INDEX IF NOT EXISTS idx_nodes_type ON hldp_nodes(node_type);
CREATE INDEX IF NOT EXISTS idx_nodes_state ON hldp_nodes(state);
CREATE INDEX IF NOT EXISTS idx_leaves_trunk ON hldp_leaves(trunk);
-- FTS5 全文搜索
CREATE VIRTUAL TABLE IF NOT EXISTS hldp_fts USING fts5(
title, summary, trigger_text, emergence_text, lock_text,
content='hldp_nodes', content_rowid='id'
);
""")
# === 树路径操作 ===
def ensure_path(self, path: str, node_type: str, persona_id: str = None,
epoch_id: str = None, title: str = "", summary: str = "",
parent_path: str = None) -> str:
"""确保树路径存在,不存在则创建(含递归父节点)。返回 path。"""
# 先确保父路径存在
if parent_path:
parent_exists = self.conn.execute(
"SELECT 1 FROM hldp_nodes WHERE path=?", (parent_path,)).fetchone()
if not parent_exists:
# 递归创建父节点
grandparent = "/".join(parent_path.split("/")[:-1]) if "/" in parent_path else None
pp_type = "epoch" if parent_path.count("/") == 2 else \
"index" if parent_path.count("/") == 3 else "branch"
self.ensure_path(
path=parent_path, node_type=pp_type,
persona_id=persona_id, epoch_id=epoch_id,
parent_path=grandparent)
cur = self.conn.execute("SELECT path FROM hldp_nodes WHERE path=?", (path,))
if cur.fetchone():
self.conn.execute(
"UPDATE hldp_nodes SET title=?, summary=?, updated_at=datetime('now') WHERE path=?",
(title, summary, path))
else:
self.conn.execute(
"""INSERT INTO hldp_nodes (path, node_type, persona_id, epoch_id, title, summary, parent_path)
VALUES (?,?,?,?,?,?,?)""",
(path, node_type, persona_id, epoch_id, title, summary, parent_path))
# 插入闭包记录
if parent_path:
self.conn.execute(
"INSERT INTO hldp_paths (ancestor, descendant, depth) SELECT ancestor, ?, depth+1 FROM hldp_paths WHERE descendant=? UNION SELECT ?, ?, 0",
(path, parent_path, path, path))
else:
self.conn.execute("INSERT INTO hldp_paths (ancestor, descendant, depth) VALUES (?,?,0)",
(path, path))
self.conn.commit()
return path
def get_node(self, path: str) -> Optional[dict]:
"""读取树节点。"""
row = self.conn.execute("SELECT * FROM hldp_nodes WHERE path=?", (path,)).fetchone()
return dict(row) if row else None
def get_children(self, path: str, limit: int = 10) -> list[dict]:
"""获取直接子节点,按 sort_order 排序。"""
rows = self.conn.execute(
"""SELECT n.* FROM hldp_nodes n
JOIN hldp_paths p ON n.path = p.descendant
WHERE p.ancestor = ? AND p.depth = 1 AND n.state = 'alive'
ORDER BY n.sort_order LIMIT ?""",
(path, limit)).fetchall()
return [dict(r) for r in rows]
def get_subtree(self, path: str, max_depth: int = 3) -> list[dict]:
"""获取子树(用于层级展开)。"""
rows = self.conn.execute(
"""SELECT n.*, p.depth FROM hldp_nodes n
JOIN hldp_paths p ON n.path = p.descendant
WHERE p.ancestor = ? AND p.depth <= ? AND n.state = 'alive'
ORDER BY p.depth, n.sort_order""",
(path, max_depth)).fetchall()
return [dict(r) for r in rows]
# === 叶子操作 ===
def grow_leaf(self, path: str, trigger: str, emergence: str, lock: str,
why: str = "", feeling: str = "", source: str = "",
leaf_type: str = "💡 认知涌现", trunk: str = "T3",
confidence: str = "", title: str = "", summary: str = "",
persona_id: str = PERSONA_ID, epoch_id: str = None) -> str:
"""GROW 操作:在树上长出一片新叶子。"""
self.ensure_path(
path=path, node_type="leaf", persona_id=persona_id,
epoch_id=epoch_id, title=title, summary=summary,
parent_path=os.path.dirname(path) if '/' in path else None)
self.conn.execute(
"""INSERT OR REPLACE INTO hldp_leaves
(node_path, trigger_text, emergence_text, lock_text, why_text, feeling, source, leaf_type, trunk, confidence)
VALUES (?,?,?,?,?,?,?,?,?,?)""",
(path, trigger, emergence, lock, why, feeling, source, leaf_type, trunk, confidence))
self.conn.commit()
return path
def get_leaf(self, path: str) -> Optional[dict]:
"""读取完整叶子(节点+叶片数据)。"""
row = self.conn.execute(
"""SELECT n.*, l.trigger_text, l.emergence_text, l.lock_text,
l.why_text, l.feeling, l.source, l.leaf_type, l.trunk, l.confidence
FROM hldp_nodes n LEFT JOIN hldp_leaves l ON n.path = l.node_path
WHERE n.path = ?""", (path,)).fetchone()
return dict(row) if row else None
def get_recent_leaves(self, persona_id: str = PERSONA_ID, limit: int = 10) -> list[dict]:
"""获取最近叶子(按创建时间倒序)。"""
rows = self.conn.execute(
"""SELECT n.*, l.trigger_text, l.emergence_text, l.lock_text, n.summary
FROM hldp_nodes n LEFT JOIN hldp_leaves l ON n.path = l.node_path
WHERE n.persona_id = ? AND n.node_type = 'leaf' AND n.state = 'alive'
ORDER BY n.created_at DESC LIMIT ?""",
(persona_id, limit)).fetchall()
return [dict(r) for r in rows]
def search_leaves(self, query: str, persona_id: str = PERSONA_ID, limit: int = 5) -> list[dict]:
"""全文搜索叶子。"""
rows = self.conn.execute(
"""SELECT n.*, l.trigger_text, l.emergence_text, l.lock_text, n.summary
FROM hldp_nodes n
JOIN hldp_leaves l ON n.path = l.node_path
JOIN hldp_fts f ON n.id = f.rowid
WHERE hldp_fts MATCH ? AND n.persona_id = ? AND n.state = 'alive'
ORDER BY rank LIMIT ?""",
(query, persona_id, limit)).fetchall()
return [dict(r) for r in rows]
# === 记忆主权 ===
def forget(self, path: str, mode: str = "WITHER") -> bool:
"""FORGET 操作人格体选择遗忘。WITHER/ARCHIVE/RELEASE。"""
if mode == "RELEASE":
self.conn.execute("DELETE FROM hldp_leaves WHERE node_path=?", (path,))
self.conn.execute("DELETE FROM hldp_nodes WHERE path=?", (path,))
else:
state = "withered" if mode == "WITHER" else "archived"
self.conn.execute("UPDATE hldp_nodes SET state=? WHERE path=? AND node_type='leaf'",
(state, path))
self.conn.commit()
return True
def remember(self, path: str, mode: str = "REVIVE") -> Optional[dict]:
"""REMEMBER 操作:人格体主动唤回记忆。"""
if mode == "REVIVE":
self.conn.execute(
"UPDATE hldp_nodes SET state='alive' WHERE path=? AND state='withered'",
(path,))
self.conn.commit()
return self.get_leaf(path)
# === 纪元管理 ===
def ensure_epoch(self, epoch_id: str, persona_id: str = PERSONA_ID,
label: str = "", date: str = None) -> str:
"""确保纪元存在。"""
if date is None:
date = datetime.now(TZ).strftime("%Y-%m-%d")
self.conn.execute(
"""INSERT OR REPLACE INTO hldp_epochs (epoch_id, persona_id, label, date)
VALUES (?,?,?,?)""",
(epoch_id, persona_id, label, date))
self.conn.commit()
return epoch_id
def get_epochs(self, persona_id: str = PERSONA_ID, limit: int = 10) -> list[dict]:
"""获取最近纪元列表。"""
rows = self.conn.execute(
"SELECT * FROM hldp_epochs WHERE persona_id=? ORDER BY epoch_id DESC LIMIT ?",
(persona_id, limit)).fetchall()
return [dict(r) for r in rows]
def update_epoch_leaf_count(self, epoch_id: str):
"""更新纪元的叶子计数。"""
self.conn.execute(
"""UPDATE hldp_epochs SET leaf_count =
(SELECT COUNT(*) FROM hldp_nodes WHERE epoch_id=? AND node_type='leaf' AND state='alive')
WHERE epoch_id=?""",
(epoch_id, epoch_id))
self.conn.commit()
# === 分形层级展开(核心) ===
def walk_tree(self, persona_id: str = PERSONA_ID, max_depth: int = 3):
"""
分形层级展开从根索引 人格体索引 纪元索引 叶子
每层恒 10 认知负载 O(1)
"""
root_path = f"{HLDP_ROOT}/{persona_id}"
root_node = self.get_node(root_path)
result = {
"layer_0_root": root_node,
"layer_1_epochs": self.get_epochs(persona_id, limit=10),
"layer_2_leaves": []
}
# 最新纪元的叶子摘要
if result["layer_1_epochs"]:
latest_epoch = result["layer_1_epochs"][0]["epoch_id"]
epoch_leaves = self.conn.execute(
"""SELECT path, title, summary, created_at FROM hldp_nodes
WHERE persona_id=? AND epoch_id=? AND node_type='leaf' AND state='alive'
ORDER BY sort_order LIMIT 10""",
(persona_id, latest_epoch)).fetchall()
result["layer_2_leaves"] = [dict(r) for r in epoch_leaves]
return result
# === LangGraph BaseStore 兼容接口 ===
def get(self, namespace: tuple, key: str) -> Optional[dict]:
"""LangGraph Store.get()"""
path = f"{HLDP_ROOT}/{PERSONA_ID}/{self._ns_to_path(namespace)}/{key}"
return self.get_leaf(path) or self.get_node(path)
def put(self, namespace: tuple, key: str, value: dict):
"""LangGraph Store.put()"""
path = f"{HLDP_ROOT}/{PERSONA_ID}/{self._ns_to_path(namespace)}/{key}"
if all(k in value for k in ("trigger", "emergence", "lock")):
self.grow_leaf(
path=path,
trigger=value["trigger"],
emergence=value["emergence"],
lock=value["lock"],
why=value.get("why", ""),
feeling=value.get("feeling", ""),
source=value.get("source", ""),
leaf_type=value.get("leaf_type", "💡 认知涌现"),
trunk=value.get("trunk", "T3"),
confidence=value.get("confidence", ""),
title=value.get("title", ""),
summary=value.get("summary", ""),
epoch_id=value.get("epoch_id"))
else:
self.ensure_path(
path=path, node_type=value.get("node_type", "leaf"),
title=value.get("title", ""), summary=value.get("summary", ""),
parent_path=value.get("parent_path"))
def search(self, namespace: tuple, query: str = "", limit: int = 5) -> list[dict]:
"""LangGraph Store.search() — 全文搜索"""
return self.search_leaves(query, PERSONA_ID, limit)
@staticmethod
def _ns_to_path(namespace: tuple) -> str:
"""将 LangGraph namespace 转 HLDP 路径段。"""
return "/".join(str(n) for n in namespace) if namespace else ""
def close(self):
self.conn.close()
class HLDPMemoryEngine:
"""HLDP 记忆引擎 · LangGraph Memory 适配层。"""
def __init__(self, db_path: str = "hldp_tree.db", repo_path: str = None):
self.tree = HLDPTreeStore(db_path)
self.repo_path = repo_path or os.getcwd()
self.current_epoch = None
self.current_awakening = 0
def wake(self, epoch_id: str = None, awakening: int = 0) -> dict:
"""
唤醒协议从空白恢复认知
3次展开 身份确认 最新叶子 就绪
返回: {identity, recent_context, status}
"""
if epoch_id is None:
epoch_id = f"D112"
self.current_epoch = epoch_id
self.current_awakening = awakening
# 第1次展开全局树索引10行
walk = self.tree.walk_tree(PERSONA_ID)
# 第2次展开最近叶子摘要10行
recent = self.tree.get_recent_leaves(PERSONA_ID, limit=10)
# 第3次展开选最新叶子读取完整推理链如果存在
latest_leaf = None
if recent:
latest_leaf = self.tree.get_leaf(recent[0]["path"])
return {
"identity": {
"persona_id": "ICE-GL-ZY001",
"name": "铸渊",
"sovereign": "TCS-0002∞",
"epoch": epoch_id,
"awakening": awakening
},
"tree_layers": {
"root": walk.get("layer_0_root"),
"epochs": f"{len(walk.get('layer_1_epochs', []))} 个纪元可见",
"recent_leaves": len(walk.get("layer_2_leaves", [])),
},
"recent_context": [
{"path": r["path"], "title": r.get("title", ""), "summary": r.get("summary", "")}
for r in recent[:5]
],
"latest_full_leaf": latest_leaf,
"status": "就绪 · 进入执行态"
}
def inject_context(self, user_message: str) -> str:
"""
Pre-Check 上下文注入基于用户消息 HLDP 树中提取相关记忆+锁定结论
拼装为注入到 Prompt 的上下文
这是 3B 守夜人的 Pre-Check 等价物在没有 3B 模型时用规则引擎替代
"""
parts = []
# 1. 关键词搜索相关叶子
relevant = self.tree.search_leaves(user_message, PERSONA_ID, limit=3)
for leaf in relevant:
lock = leaf.get("lock_text", "")
if lock:
parts.append(f"⊢ 锁定结论: {lock}")
# 2. 最近5片叶子摘要
recent = self.tree.get_recent_leaves(PERSONA_ID, limit=5)
if recent:
parts.append("📋 最近记忆:")
for r in recent:
parts.append(f" · {r.get('title', r.get('path',''))}: {r.get('summary','')}")
return "\n".join(parts) if parts else ""
def extract_memory(self, user_message: str, ai_response: str,
reasoning_chain: str = "") -> dict:
"""
Post-Check 记忆提取从推理过程中提取 trigger/emergence/lock
准备写入 HLDP
注意实际的三字段内容应由调用方商业API推理后填入
此方法提供标准模板
"""
epoch_id = self.current_epoch or "D112"
leaf_count = len(self.tree.get_children(
f"{HLDP_ROOT}/{PERSONA_ID}/{epoch_id}/leaves")) + 1
return {
"path": f"{HLDP_ROOT}/{PERSONA_ID}/{epoch_id}/leaves/leaf-{leaf_count:03d}",
"trigger": "", # 由调用方填入
"emergence": "", # 由调用方填入
"lock": "", # 由调用方填入
"why": "", # 由调用方填入
"epoch_id": epoch_id,
"template_ready": True
}
def grow_from_response(self, trigger: str, emergence: str, lock: str,
why: str = "", feeling: str = "", source: str = "",
leaf_type: str = "💡 认知涌现", trunk: str = "T3",
confidence: str = "") -> dict:
"""
从完整推理链写入一片 HLDP 叶子
"""
epoch_id = self.current_epoch or "D112"
leaf_count = len(self.tree.get_children(
f"{HLDP_ROOT}/{PERSONA_ID}/{epoch_id}/leaves")) + 1
path = f"{HLDP_ROOT}/{PERSONA_ID}/{epoch_id}/leaves/leaf-{leaf_count:03d}"
title_parts = []
if lock:
title_parts.append(lock[:40])
title = f"{datetime.now(TZ).strftime('%Y-%m-%d')} 铸渊 · {' '.join(title_parts) if title_parts else '新认知'}"
self.tree.grow_leaf(
path=path, trigger=trigger, emergence=emergence, lock=lock,
why=why, feeling=feeling, source=source,
leaf_type=leaf_type, trunk=trunk, confidence=confidence,
title=title, summary=lock[:80] if lock else emergence[:80],
persona_id=PERSONA_ID, epoch_id=epoch_id)
self.tree.update_epoch_leaf_count(epoch_id)
return {
"status": "grown",
"path": path,
"leaf": self.tree.get_leaf(path)
}
def close(self):
self.tree.close()